ProductPromotion
Logo

Elixir

made by https://0x3d.site

GitHub - slashdotdash/eventsourced: Functional domain models with event sourcing in Elixir
Functional domain models with event sourcing in Elixir - slashdotdash/eventsourced
Visit Site

GitHub - slashdotdash/eventsourced: Functional domain models with event sourcing in Elixir

GitHub - slashdotdash/eventsourced: 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)

Articles
to learn more about the elixir concepts.

Resources
which are currently available to browse on.

mail [email protected] to add your project or resources here 🔥.

FAQ's
to know more about the topic.

mail [email protected] to add your project or resources here 🔥.

Queries
or most google FAQ's about Elixir.

mail [email protected] to add more queries here 🔍.

More Sites
to check out once you're finished browsing here.

0x3d
https://www.0x3d.site/
0x3d is designed for aggregating information.
NodeJS
https://nodejs.0x3d.site/
NodeJS Online Directory
Cross Platform
https://cross-platform.0x3d.site/
Cross Platform Online Directory
Open Source
https://open-source.0x3d.site/
Open Source Online Directory
Analytics
https://analytics.0x3d.site/
Analytics Online Directory
JavaScript
https://javascript.0x3d.site/
JavaScript Online Directory
GoLang
https://golang.0x3d.site/
GoLang Online Directory
Python
https://python.0x3d.site/
Python Online Directory
Swift
https://swift.0x3d.site/
Swift Online Directory
Rust
https://rust.0x3d.site/
Rust Online Directory
Scala
https://scala.0x3d.site/
Scala Online Directory
Ruby
https://ruby.0x3d.site/
Ruby Online Directory
Clojure
https://clojure.0x3d.site/
Clojure Online Directory
Elixir
https://elixir.0x3d.site/
Elixir Online Directory
Elm
https://elm.0x3d.site/
Elm Online Directory
Lua
https://lua.0x3d.site/
Lua Online Directory
C Programming
https://c-programming.0x3d.site/
C Programming Online Directory
C++ Programming
https://cpp-programming.0x3d.site/
C++ Programming Online Directory
R Programming
https://r-programming.0x3d.site/
R Programming Online Directory
Perl
https://perl.0x3d.site/
Perl Online Directory
Java
https://java.0x3d.site/
Java Online Directory
Kotlin
https://kotlin.0x3d.site/
Kotlin Online Directory
PHP
https://php.0x3d.site/
PHP Online Directory
React JS
https://react.0x3d.site/
React JS Online Directory
Angular
https://angular.0x3d.site/
Angular JS Online Directory