noddde
Design Decisions

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@Aggregate annotation + @CommandHandler/@EventSourcingHandler methods (Java annotations / TS decorators)
  • NestJS CQRS — Class-based aggregates with AggregateRoot base class
  • EventStoreDB client — Thin client, no aggregate abstraction at all

Why This Approach

The Decider pattern treats an aggregate as three pure components:

bank-account/bank-account.ts
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 decide and evolve rather 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.

On this page