Why noddde
A 90-second read for engineers evaluating a TypeScript DDD framework
This page is for engineers evaluating noddde against the alternatives. It is short, opinionated, and does not try to win every argument.
The pitch
- Type safety. A single typed bundle drives every handler signature — commands, events, evolve, projections, queries, saga reactions — and the discriminated unions reach
dispatchCommandanddispatchQuerywithout a single manual generic. - Pure
decideandevolve.decide(command, state, infra) => events,evolve(event, state) => state. Infrastructure is a parameter; tests call handlers directly, no runtime to bootstrap. - CQRS and process management as first-class citizens. Aggregates, projections (views + queries), and sagas (event-driven process managers) are peers — same definition pattern, same wiring. Event-sourced and state-stored aggregates coexist in one domain.
- Production runtime, included. Kafka / NATS / RabbitMQ buses with broker resilience. Drizzle / Prisma / TypeORM persistence. Transactional outbox with at-least-once delivery, command idempotency, snapshot stores, optimistic or pessimistic concurrency, and typed upcaster chains for event versioning.
- Audit and tracing, native. Every event ships with
eventId,correlationId,causationId,userId,aggregateId, andsequenceNumber. OpenTelemetry is dynamically detected — install@opentelemetry/apiand W3C Trace Context propagates through event metadata; without it, every span call is a zero-cost no-op. - Adopt as much as you want.
@noddde/coreis types and identity functions, zero dependencies.@noddde/engineis the runtime.@noddde/nestjsexposes theDomainas a global Nest provider.@noddde/cliscaffolds aggregates, projections, sagas — and emits Mermaid / DOT / JSON diagrams of your domain's command / event / query graph.
Compared to the obvious alternatives
Hand-rolled TypeScript DDD
What most teams actually do. You write your own Command/Event discriminated unions, your own Aggregate base class or factory, your own command/event registries, your own persistence interface, your own outbox.
The aggregate file looks roughly the same length as a noddde aggregate. The problem is everything around it: the registries you maintain by hand, the type plumbing between the command bus and the aggregate, the state-stored vs. event-sourced abstraction, the snapshot store, the saga state machine, the projection rebuild, the OTel spans. That is the code noddde replaces — not the aggregate itself.
Pick noddde over hand-rolling when you are building more than one aggregate and you do not want to be in the framework-maintenance business.
Stay hand-rolled when you have one aggregate, exotic persistence requirements, or you genuinely want full control of the wiring.
NestJS CQRS
The dominant TypeScript DDD framework. Class-based, decorator-based, DI container, reflect-metadata. @CommandHandler(MyCommand), @EventsHandler(MyEvent), classes extending AggregateRoot, manual apply() calls inside command handlers.
Differences in approach:
| NestJS CQRS | noddde | |
|---|---|---|
| Aggregate | class extends AggregateRoot | object via defineAggregate |
| Command handler | Decorated method on a class | Entry in a typed decide map |
| Event application | this.apply(event) mutates this | evolve(event, state) => state (pure) |
| State changes | In-place mutation | Immutable new state |
| Type safety | Manual generics on every class | Inferred from one Def type |
| Testing | Bootstrap a Nest module | Call functions directly |
| Runtime requirement | Nest application + DI container | None. Engine is opt-in. |
You do not have to leave NestJS to use noddde. @noddde/nestjs registers the noddde Domain as a global Nest provider, so controllers and services inject and dispatch through standard Nest DI. The choice is between NestJS CQRS's class+decorator shape and noddde's functional shape inside the same Nest app, not between Nest and noddde.
Pick noddde over NestJS CQRS when you want the discipline of pure functions, prefer inference to manual generics, and would rather express domain logic as data than as decorated classes.
Stay on NestJS CQRS when the team is fluent in its patterns and the cost of introducing a new abstraction outweighs the gains.
Emmett
The closest active functional prior art. Like noddde, models commands and events as pure data and decisions as pure functions for TypeScript. Backed by Oskar Dudycz, who has been writing about event sourcing for years.
Differences in practice:
| Emmett | noddde | |
|---|---|---|
| Persistence | Event-sourcing first; EventStoreDB and PostgreSQL adapters | Event-sourced and state-stored aggregates as first-class peers |
| Type model | Commands, events, state declared as separate generics | One Def bundle drives every handler's inference |
| Composition | À-la-carte: assemble the pieces you need | defineDomain + wireDomain as a unified orchestrator |
| Sagas / projections | First-class but wired up externally | Defined in the same Def ecosystem; saga reactions return commands |
| Adapters | EventStoreDB, PostgreSQL, in-memory | Drizzle, Prisma, TypeORM, in-memory; pluggable event bus |
| Tracing | Application-level | Native OpenTelemetry; W3C Trace Context propagated through event metadata |
| Transactional outbox | Application-level | Built into the engine: outbox store + relay loop |
| Command idempotency | Application-level | Built-in IdempotencyStore keyed on commandId |
| CLI | — | @noddde/cli scaffolds aggregates, projections, sagas, and full domains |
Pick noddde over Emmett when you want one domain definition that wires aggregates, projections, sagas, and infrastructure together, and when state-stored aggregates need to coexist with event-sourced ones in the same domain.
Pick Emmett when you prefer a more proven event-sourcing-first library backed by an established author and EventStoreDB community.
Effect / a general-purpose effect library
Effect is excellent for typed errors and effect composition. It is not a DDD framework. You can build DDD on top of Effect, but you write the aggregate patterns, the command bus, the projection runtime, the saga state machine, etc. yourself.
Pick noddde over Effect when the value you want is the DDD shape (decide/evolve, projections, sagas), not effect composition.
Use both when you want noddde's domain shape and Effect's error/effect ergonomics inside your handlers. They compose — decide and evolve are just functions.
Wolkenkit
The most direct prior art for "functional CQRS+ES in TypeScript." Wolkenkit pioneered a lot of these ideas. It is largely dormant in 2026 and its runtime is heavier than noddde's. If you liked Wolkenkit's shape, noddde is a smaller, actively-maintained version of the same idea.
When noddde is the wrong choice
- You need a CRUD app. DDD/CQRS/ES is overkill for forms-over-data. Use Drizzle and a router.
- You need a polyglot event store with cross-language consumers. noddde's event types are TypeScript-shaped. Cross-language consumers can read events, but the schema discipline lives outside the framework.
- You want Java-style DDD with rich domain objects. noddde is functional. Aggregates are state + handler maps, not objects with methods. If
account.deposit(100)reads more naturally to your team thandecide.Deposit(command, state), that is a real preference and noddde will fight you.
Why these design decisions
The shape of noddde is the result of specific tradeoffs, each documented:
- Why the Decider pattern — pure functions over
this.apply()mutation - Why a single
AggregateTypesbundle — oneDefinstead of five generic parameters - Why infrastructure is a function parameter — handlers receive what they need; tests pass the rest
- Why commands return events — pushing persistence and publishing out of business logic
- Why two persistence strategies — event-sourced and state-stored, both first-class
Next steps
- Quick Start — see a complete aggregate end-to-end
- Core Concepts — the foundational patterns
- Hotel booking sample — a real-shaped domain that exercises >90% of the framework