noddde

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 dispatchCommand and dispatchQuery without a single manual generic.
  • Pure decide and evolve. 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, and sequenceNumber. OpenTelemetry is dynamically detected — install @opentelemetry/api and 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/core is types and identity functions, zero dependencies. @noddde/engine is the runtime. @noddde/nestjs exposes the Domain as a global Nest provider. @noddde/cli scaffolds 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 CQRSnoddde
Aggregateclass extends AggregateRootobject via defineAggregate
Command handlerDecorated method on a classEntry in a typed decide map
Event applicationthis.apply(event) mutates thisevolve(event, state) => state (pure)
State changesIn-place mutationImmutable new state
Type safetyManual generics on every classInferred from one Def type
TestingBootstrap a Nest moduleCall functions directly
Runtime requirementNest application + DI containerNone. 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:

Emmettnoddde
PersistenceEvent-sourcing first; EventStoreDB and PostgreSQL adaptersEvent-sourced and state-stored aggregates as first-class peers
Type modelCommands, events, state declared as separate genericsOne Def bundle drives every handler's inference
CompositionÀ-la-carte: assemble the pieces you needdefineDomain + wireDomain as a unified orchestrator
Sagas / projectionsFirst-class but wired up externallyDefined in the same Def ecosystem; saga reactions return commands
AdaptersEventStoreDB, PostgreSQL, in-memoryDrizzle, Prisma, TypeORM, in-memory; pluggable event bus
TracingApplication-levelNative OpenTelemetry; W3C Trace Context propagated through event metadata
Transactional outboxApplication-levelBuilt into the engine: outbox store + relay loop
Command idempotencyApplication-levelBuilt-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 than decide.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:

Next steps

On this page