eventsourced
eventsourced copied to clipboard
Define valid aggregate states and applicable commands using an finite state machine
trafficstars
An aggregate usually follows a lifecycle where it transitions between a finite set of states in response to received commands. Instead of manually adding a state field and verifying the aggregate is in a valid state when handling a command it could use an FSM.
defmodule BankAccount do
use EventSourced.AggregateRoot,
fields: [account_number: nil, balance: nil],
initial_state: :unopened,
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}
defstate unopened do
defcommand open_account(account_number, initial_balance), account do
account
|> update(%BankAccountOpened{account_number: account_number, initial_balance: initial_balance})
end
end
defstate open do
defcommand deposit(amount), account when amount > 0 do
balance = account.state.balance + amount
account
|> update(%MoneyDeposited{amount: amount, balance: balance})
end
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,
state: :open,
}
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
Saša Jurić's fsm library would be a good fit.