Concepts / Building / Deciders
Building

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
  • decide takes 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.
  • evolve takes 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; evolve only records its consequences.
  • initialState is 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:

  1. Rebuild the state by folding the past events of the book over the initial state with evolve.
  2. Decide by calling decide with the command and that state. If the book is already borrowed, the result is a rejection; otherwise, it is a book-borrowed event.
  3. 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) → Events says everything about what decide does. 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 decide dispatching each command and evolve routing 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 decide function 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 decide and evolve are 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 decide returns.

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.