• Stars
    star
    104
  • Rank 330,604 (Top 7 %)
  • Language
    Elixir
  • License
    MIT License
  • Created almost 9 years ago
  • Updated over 7 years ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

Functional domain models with event sourcing in Elixir

Functional Domain Models with Event Sourcing in Elixir

Build functional, event-sourced domain models.

  • Aggregate root public methods accept the current state and a command, returning the new state (including any applied events).
  • Aggregate root state is rebuilt from events by applying a reduce function, starting from an empty state.

MIT License

Build Status

Creating a new aggregate root and invoking command functions

account =
  BankAccount.new("1234")
  |> BankAccount.open_account("ACC123", 100)
  |> BankAccount.deposit(50)
  |> BankAccount.withdraw(75)

Populating an aggregate root from a given list of events

events = [
  %BankAccountOpened{account_number: "ACC123", initial_balance: 100},
  %MoneyDeposited{amount: 50, balance: 150},
  %MoneyWithdrawn{amount: 75, balance: 75}
]

account = BankAccount.load("1234", events)

Event-sourced domain model

State may only be updated by applying an event. This is to allow internal state to be reconstituted by replaying a list of events. We Enum.reduce the events against the empty state.

An apply/2 function must exist for each event the aggregate root may publish. It expects to receive the aggregate's state (e.g. %BankAccount.State{}) and the event (e.g. %BankAccount.Events.MoneyDeposited{}). It is responsible for updating the internal state using fields from the event.

Using the EventSourced.AggregateRoot macro, the example bank account example listed above is implemented as follows.

defmodule BankAccount do
  use EventSourced.AggregateRoot, fields: [account_number: nil, balance: nil]

  defmodule Events do
    defmodule BankAccountOpened do
      defstruct account_number: nil, initial_balance: nil
    end

    defmodule MoneyDeposited do
      defstruct amount: nil, balance: nil
    end

    defmodule MoneyWithdrawn do
      defstruct amount: nil, balance: nil
    end
  end

  alias Events.{BankAccountOpened,MoneyDeposited,MoneyWithdrawn}

  def open_account(%BankAccount{} = account, account_number, initial_balance) when initial_balance > 0 do
    account
    |> update(%BankAccountOpened{account_number: account_number, initial_balance: initial_balance})
  end

  def deposit(%BankAccount{} = account, amount) when amount > 0 do
    balance = account.state.balance + amount

    account
    |> update(%MoneyDeposited{amount: amount, balance: balance})
  end

  def withdraw(%BankAccount{} = account, amount) when amount > 0 do
    balance = account.state.balance - amount

    account
    |> update(%MoneyWithdrawn{amount: amount, balance: balance})
  end

  # event handling callbacks that mutate state

  def apply(%BankAccount.State{} = state, %BankAccountOpened{} = account_opened) do
    %BankAccount.State{state |
      account_number: account_opened.account_number,
      balance: account_opened.initial_balance
    }
  end

  def apply(%BankAccount.State{} = state, %MoneyDeposited{} = money_deposited) do
    %BankAccount.State{state |
      balance: money_deposited.balance
    }
  end

  def apply(%BankAccount.State{} = state, %MoneyWithdrawn{} = money_withdrawn) do
    %BankAccount.State{state |
      balance: money_withdrawn.balance
    }
  end
end

This is an entirely functional event-sourced aggregate root.

Testing

The domain models can be simply tested by invoking a public command method and verifying the correct event(s) have been applied.

test "deposit money" do
  account =
    BankAccount.new("123")
    |> BankAccount.open_account("ACC123", 100)
    |> BankAccount.deposit(50)

  assert account.pending_events == [
    %BankAccountOpened{account_number: "ACC123", initial_balance: 100},
    %MoneyDeposited{amount: 50, balance: 150}
  ]
  assert account.state == %BankAccount.State{account_number: "ACC123", balance: 150}
  assert account.version == 2
end

Handling business rule violations

Return :ok or :error tuples

This is the most common and idiomatic Elixir approach to writing functions that may error.

The aggregate root must return either an {:ok, aggregate} or {:error, reason} tuple from each public API function on success or failure.

defmodule BankAccount do
  use EventSourced.AggregateRoot, fields: [account_number: nil, balance: nil]

  # ... event and command definition as above

  def open_account(%BankAccount{} = account, account_number, initial_balance) when initial_balance <= 0 do
    {:error, :initial_balance_must_be_above_zero}
  end

  def open_account(%BankAccount{} = account, account_number, initial_balance) when initial_balance > 0 do
    {:ok, update(account, %BankAccountOpened{account_number: account_number, initial_balance: initial_balance})}
  end
end

Following this approach allows strict pattern matching on success and failures. An error indicates a domain business rule violation, such as attempting to open an account with a negative initial balance.

You cannot use the pipeline operator (|>) to chain the functions. Use the with special form instead. This is demonstrated in the example below.

with account <- BankAccount.new("123"),
  {:ok, account} <- BankAccount.open_account(account, "ACC123", 100),
  {:ok, account} <- BankAccount.deposit(account, 50),
do: account

Raise an exception

Prevent the aggregate root function from successfully executing by using one of the following tactics.

  • Use guard clauses and pattern matching on functions to prevent invalid invocation.
  • Raise an exception when a business rule violation is encountered.
defmodule BankAccount do
  use EventSourced.AggregateRoot, fields: [account_number: nil, balance: nil]

  # ... event and command definition as above

  defmodule InvalidOpeningBalanceError do
    defexception message: "initial balance must be above zero"
  end

  def open_account(%BankAccount{} = account, account_number, initial_balance) when initial_balance <= 0 do
    raise InvalidOpeningBalanceError
  end

  def open_account(%BankAccount{} = account, account_number, initial_balance) when initial_balance > 0 do
    update(account, %BankAccountOpened{account_number: account_number, initial_balance: initial_balance})
  end
end

This allows you to use the pipeline operator (|>) to chain functions.

account =
  BankAccount.new("123")
  |> BankAccount.open_account("ACC123", 100)
  |> BankAccount.deposit(50)

More Repositories

1

awesome-elixir-cqrs

A curated list of awesome Elixir and Command Query Responsibility Segregation (CQRS) resources.
704
star
2

jekyll-lunr-js-search

[UNSUPPORTED] Jekyll + lunr.js = static websites with powerful full-text search using JavaScript
JavaScript
549
star
3

conduit

RealWorld example backend implementing the CQRS/ES pattern in Elixir and Phoenix
Elixir
347
star
4

faker-cs

C# port of the Ruby Faker gem (http://faker.rubyforge.org/)
C#
241
star
5

node-ledger-web

Web front-end to access ledger cli data.
JavaScript
144
star
6

saas-startup-checklist

SaaS Startup Checklist
128
star
7

phoenix-react-redux-example

Phoenix framework example using React and Redux
JavaScript
110
star
8

rcqrs

Ruby CQRS with Event Sourcing library
Ruby
98
star
9

cqrs-best-practices

Best practices, guidance, and anti-patterns to avoid when building an application following CQRS/ES principles
95
star
10

node-pipes-and-filters

Pipes and Filters for Node.js to divide a larger processing task into a sequence of smaller, independent processing steps (Filters) that are connected by channels (Pipes).
JavaScript
80
star
11

segment-challenge

Segment Challenge is an Elixir Phoenix web application built using Commanded
Elixir
76
star
12

node-ledger

API for the ledger command-line interface (ledger-cli.org).
JavaScript
72
star
13

strava

Elixir wrapper for the Strava API (v3)
Elixir
46
star
14

til

Today I Learned
Elixir
44
star
15

eventstore-migrator

Copy & transform migration strategy for Elixir EventStore
Elixir
31
star
16

node-ledger-rest

REST web service to access the Ledger command-line interface (http://ledger-cli.org/).
JavaScript
27
star
17

stateless

[DEPRECATED] A C# Hierarchical State Machine
C#
23
star
18

Treaty

Rules Engine written in TypeScript implementing the Rete algorithm
20
star
19

domain-driven-js

Domain-driven JavaScript
20
star
20

gift-card-demo

Commanded demo application focused around a simple giftcard domain.
Elixir
20
star
21

implementing-cqrs-in-elixir

An introduction to implementing Command Query Responsibility Segregation (CQRS) in Elixir applications.
17
star
22

node-ledger-import

Import accounting transactions from CSV files to Ledger format.
JavaScript
11
star
23

cqrs-journey-pdf

CQRS Journey Guide converted to PDF for eBook reading.
Ruby
11
star
24

rcqrs-rails

Rails 3 plugin to use the RCQRS library
Ruby
9
star
25

rcqrs-blog

Rails 3 blog app using the RCQRS library
JavaScript
9
star
26

typescript-intro-presentation

Introduction to TypeScript presentation
JavaScript
2
star
27

Duplicity

Duplicate file system changes from one directory to another.
C#
2
star
28

kv

Key/value store using Elixir and Commanded CQRS/ES
Elixir
2
star
29

calculator

Attempt to define supported operations using `before_compile` macro
Elixir
2
star
30

DotNetFlow

An ASP.NET MVC web application for sharing links that demonstrates using CQRS with event sourcing. Inspired by RubyFlow.
JavaScript
2
star
31

eventstore-export

Export events stored in an EventStore database to disk
Elixir
2
star
32

notice

Notice board for tasks
JavaScript
1
star
33

ledger-import

Ruby
1
star
34

commanded_aggregate_race

Elixir
1
star
35

broadway-show

Broadway experiment using a custom `GenStage` producer with partitioning which stops processing on message failure
Elixir
1
star
36

nerves_agile_octopus

Display Agile Octopus electricity prices on an Inky pHAT display connected to a Raspberry Pi using Nerves.
Elixir
1
star