Why the Decider Pattern?
Why noddde uses pure functions instead of OOP aggregate classes.
The Decision
noddde models aggregates as pure function Deciders — initialState + decide + evolve — instead of class hierarchies with base classes and method decorators.
The Problem
Traditional DDD frameworks model aggregates as classes that extend a base AggregateRoot, mutate private state via this, and use decorators like @CommandHandler / @EventSourcingHandler to wire methods. This couples your domain to the framework, mixes decision-making with state mutation, and makes testing require full class instantiation. See The Decider Pattern — OOP vs Decider for the full side-by-side comparison.
Alternatives Considered
- Axon Framework style —
@Aggregateannotation +@CommandHandler/@EventSourcingHandlermethods (Java annotations / TS decorators) - NestJS CQRS — Class-based aggregates with
AggregateRootbase class - EventStoreDB client — Thin client, no aggregate abstraction at all
Why This Approach
The Decider pattern treats an aggregate as three pure components:
const BankAccount = defineAggregate<BankAccountTypes>({
initialState: { balance: 0, transactions: [] },
decide: {
AuthorizeTransaction: (command, state, infra) => {
/* return events */
},
},
evolve: {
TransactionAuthorized: (event, state) => {
/* return new state */
},
},
});Benefits:
- No base class — Your domain types are plain TypeScript
- No decorators — Works with any TypeScript version and build tool
- No
this— Handlers are functions, not methods; no binding issues - Pure functions — Decide handlers return events; evolve handlers return state
- Trivial testing — Call the function, assert the result
- Full type inference — TypeScript infers handler parameter types automatically
- Composable — Aggregate definitions are plain objects; they can be merged, transformed, or generated
Trade-offs
- Less familiar — Developers coming from Java/C# DDD frameworks expect class-based aggregates
- Different mental model — Thinking in terms of
decideandevolverather than methods and mutations - No lifecycle hooks — No
onLoad,onSave, or similar hooks (by design — these belong in the framework, not the domain)
Example
For a worked side-by-side of an OOP BankAccount class and the equivalent Decider definition, see The Decider Pattern — OOP vs Decider. The Decider version is shorter, testable without framework setup, and makes the decision flow explicit.