The Decider Pattern
View Source< LiveView Integration | Up: Recipes | Index | Batch Loading >
Event-sourced domain logic using Command + Writer. The pattern
separates decision-making from event persistence — commands produce
events, events are persisted, and state is derived from the event stream.
Core idea
A decider is a function (command, state) -> events. It's pure: given
a command and current state, it produces a list of events. No database,
no side effects.
defmodule BankAccount do
def decide(%OpenAccount{initial_balance: balance}, nil) do
if balance < 0 do
[{:error, :negative_balance}]
else
[%AccountOpened{balance: balance}]
end
end
def decide(%Deposit{amount: amount}, state) do
[%AmountDeposited{amount: amount}]
end
def decide(%Withdraw{amount: amount}, state) do
if amount > state.balance do
[{:error, :insufficient_funds}]
else
[%AmountWithdrawn{amount: amount}]
end
end
endEvolve state from events
def evolve(nil, %AccountOpened{balance: balance}), do: %{balance: balance}
def evolve(state, %AmountDeposited{amount: amount}), do: %{state | balance: state.balance + amount}
def evolve(state, %AmountWithdrawn{amount: amount}), do: %{state | balance: state.balance - amount}Putting it together with effects
Command dispatches to the decider. Writer collects events:
defcomp handle_command(cmd) do
state <- Reader.ask() # load state
events <- Command.execute({BankAccount, :decide, cmd, state}) # decide
_ <- Writer.tell(:events, events) # collect events
new_state = Enum.reduce(events, state, &evolve/2)
_ <- Reader.local(fn _ -> new_state end, fn -> # update state
{:ok, _} <- State.put(new_state) # persist state
events
end)
endThe decider is pure. The effectful code handles loading state, persisting events, and updating projections. Swap handlers to test:
handle_command(%Deposit{amount: 100})
|> Reader.with_handler(%{balance: 50})
|> Command.with_handler(fn {mod, fun, cmd, state} -> apply(mod, fun, [cmd, state]) end)
|> Writer.with_handler([], tag: :events, output: fn r, events -> {r, Enum.reverse(events)} end)
|> State.with_handler(nil)
|> Throw.with_handler()
|> Comp.run!()
# => {[{:ok, %{balance: 150}}], [%AmountDeposited{amount: 100}]}< LiveView Integration | Up: Recipes | Index | Batch Loading >