Deciders
This guide explains the Decider pattern: the write side of an event-sourced component as a small set of pure functions. It covers how the functions work together, why the decision and the state transition belong apart, and what the pattern changes for testing and structure.
Every event-sourced component does two things. It decides which events a command leads to, and it evolves its state from the events that have happened. Traditional aggregates bundle both into one class, with a method per command and a method per event, and shared mutable state in between. The Decider pattern, introduced by Jérémie Chassaing, separates them into plain functions with explicit signatures – nothing more is needed.
The Three Parts#
A decider consists of two functions and a value:
decide: (Command, State) → Events
evolve: (State, Event) → State
initialState: State
decidetakes a command and the current state, and returns the events that describe what happens – or a rejection. This is where all business rules live. It changes nothing; it only decides.evolvetakes the current state and a single event, and returns the next state. It contains no business rules and no validation. The decision has already been made;evolveonly records its consequences.initialStateis the state before anything has happened.
How They Work Together#
For a book in a library, the state might say whether the book has been acquired and whether it is currently borrowed. When a command such as borrow book arrives, the same few steps always follow:
- Rebuild the state by folding the past events of the book over the initial state with
evolve. - Decide by calling
decidewith the command and that state. If the book is already borrowed, the result is a rejection; otherwise, it is abook-borrowedevent. - Store the new events.
The same two functions do everything: evolve rebuilds the state from the history and keeps it up to date after new events, and decide applies the rules. There is no separate mechanism for replaying and another for handling commands – it is one loop: decide, evolve, repeat.
A Functional Core#
The Decider pattern is the event-sourced form of a broader principle, often called Functional Core, Imperative Shell: keep the logic that makes decisions free of side effects, and push everything that talks to the outside world – databases, clocks, networks – to a thin shell around it.
In a decider, the functional core is decide and evolve. The shell does the rest: it reads the events of a subject, calls the functions, and writes the new events, together with the preconditions that make sure nothing was written in between. Whatever a decision depends on must reach the core as a value. If a loan period depends on the current date, the date is part of the command or the state – decide never asks a clock itself.
This split has a simple consequence: the core can be understood, tested, and changed without any infrastructure, and the shell can be replaced without touching a single business rule.
Why the Separation Pays Off#
Keeping decide and evolve apart is not only tidier. It has practical effects that grow over time:
- Testing needs no mocks. Since both functions are pure, a test is a sentence: given these past events, when this command arrives, then expect these events – or this rejection. There is no database to fake and no clock to freeze – see Testing Event-Sourced Systems.
- The contract is visible. The signature
(Command, State) → Eventssays everything about whatdecidedoes. There is no hidden state, no lifecycle, and no order of initialization to know about. - Nothing grows unnoticed. Aggregates tend to attract logic until they become hard to split. A decider has no class to grow into – adding behavior means adding a case to a function whose inputs and outputs stay explicit.
- Deciders compose. Two deciders with the same shape can be combined into one, with
decidedispatching each command andevolverouting each event to the right part.
What You Keep#
The pattern is sometimes mistaken for giving up the structure that aggregates provide. It does not:
- The boundary stays. Only the
decidefunction of a component produces its events. The encapsulation is expressed through a function signature instead of a class, but it is just as strict. - Any language works. The pattern was introduced in F#, but
decideandevolveare plain functions in TypeScript, Go, Java, or C# alike. It is about separating concerns, not about a programming paradigm. - Nothing is lost in expressiveness. Complex rules, validations across several fields, and commands that produce several events are all a matter of what
decidereturns.
Deciders and Consistency#
A decider decides on the state of one subject – one book, one reader, one loan. The shell makes sure that the decision is still valid when its events are written: if another command changed the subject in between, the write fails instead of being based on a stale state. Where a rule spans several subjects, the same idea extends to consistency boundaries that are defined per decision rather than per aggregate – see Dynamic Consistency Boundaries (DCBs).
Try it with ArchitectureKit
In ArchitectureKit, a decider is a value that combines the state of a subject with a Decide function, and executing a command reads the events, evolves the state, decides, and writes with the command's preconditions – see Making Decisions.
Designing Deciders#
Keep decide about rules and evolve about facts. If evolve starts to validate, a rule has ended up in the wrong place; if decide starts to change state, a fact has. Give the state only what decisions need – it is not a read model, and nothing outside the decider has to see it. And pass everything the decision depends on as a value, so that the core stays pure.
Seen this way, the Decider pattern is not a new architecture. It gives names and types to what every event-sourced system already does – and in doing so, makes it easy to see, test, and change.