eventsourced alternatives and similar packages
Based on the "Macros" category.
Alternatively, view eventsourced alternatives based on common mentions on social networks and blogs.
-
rulex
This tiny library (2 macros only) allows you to define very simple rule handler using Elixir pattern matching. -
apix
Simple convention and DSL for transformation of elixir functions to an API for later documentation and or validation. -
Bang
Bang simply adds dynamic bang! functions to your existing module functions with after-callback.
InfluxDB - Purpose built for real-time analytics at any scale.
Do you think we are missing an alternative of eventsourced or a related project?
Popular Comparisons
README
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
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)
*Note that all licence references and agreements mentioned in the eventsourced README section above
are relevant to that project's source code only.