Functional DDD for TypeScript

Domain logic as types and pure functions

Aggregates, projections, and sagas as typed bundles paired with pure functions for state transitions. Inference flows end to end from a single Def per aggregate.

The same domain, mapped step by step

Both sides handle the same four-step lifecycle — hydrate the aggregate, enforce invariants, update state, publish. The difference is who writes which step.

1

Hydrate

Load the aggregate's state before deciding anything.

Without noddde
const account = await this.accounts.findById(cmd.accountId);

Your repository loads events and replays them through apply().

With noddde
Handled by the framework

Framework loads events from the configured persistence and folds them through evolve before calling decide.

2

Enforce invariants

Check business rules and produce the resulting events.

Without noddde
deposit(amount: number) {
  if (amount <= 0) {
    throw new Error("Amount must be positive");
  }
  this.apply({ type: "Deposited", amount });
}

Invariant and event recording live inside an instance method that mutates this.

With noddde
decide: {
  Deposit: (cmd) => {
    if (cmd.payload.amount <= 0) {
      throw new Error("Amount must be positive");
    }
    return { name: "Deposited", payload: cmd.payload };
  },
}

Pure function: same command + state produces the same events every time.

3

Update state

Fold each event into the next state.

Without noddde
private apply(event: DomainEvent) {
  if (event.type === "Deposited") {
    this.balance += event.amount;
  }
  this.uncommitted.push(event);
}

Mutation hidden inside apply(), which also accumulates events on the instance.

With noddde
evolve: {
  Deposited: ({ amount }, state) => ({
    balance: state.balance + amount,
  }),
}

Pure function called once per recorded event; no instance state to track.

4

Publish

Hand events to the event bus for projections and sagas.

Without noddde
for (const event of account.pullEvents()) {
  await this.events.publish(event);
}

Drain pullEvents() and call the event bus from the use-case.

With noddde
Handled by the framework

Framework appends to the event store and dispatches to the event bus in the same transaction.

You write decide and evolve. Hydration and publishing become configuration.

Everything you need to model your domain

Aggregates, projections, and sagas — typed objects with handler maps and pure functions for every state transition.

Decider Pattern

Pure functions for state transitions. initialState + decide + evolve — nothing else.

CQRS

Separate command and query models with dedicated buses and handlers.

Event Sourcing

Full audit trail of state changes. Replay and rebuild state from events.

Projections

Materialized read models built from event streams, always up to date.

Sagas

Coordinate long-running business processes across aggregates.

Type-Safe

Inference flows from a single Def bundle through commands, events, handlers, and the dispatch API.