Status: Pre-1.0 Release Candidate. The core API is stable. Outbox, Graceful Shutdown, and distributed event bus adapters (RabbitMQ, NATS, Kafka) are shipped. We are currently focused on error isolation and developer ergonomics ahead of v1.0.
Building a CQRS and Event Sourced system in TypeScript usually involves significant boilerplate. Developers often end up extending AggregateRoot base classes, decorating methods with @CommandHandler(), wiring up DI containers, and working around the type system.
noddde starts from a different premise: an aggregate is just a value.
Based on the functional Decider pattern, noddde relies on pure functions and type inference rather than decorators and reflection. It provides the enterprise-grade infrastructure (Transactional Outbox, Upcasters, Unit of Work) required for real-world deployments.
Most TypeScript frameworks force you into a corner: either drown in OOP boilerplate (classes, decorators, and DI containers) or commit your entire database to an append-only Event Store. noddde offers a pragmatic, functional escape hatch.
- DDD Without the OOP Boilerplate: Say goodbye to extending
AggregateRootor fighting with@CommandHandler()decorators.nodddeis based entirely on the functional Decider pattern. Aggregates and Sagas are just pure functions, making your core domain incredibly easy to reason about and test. - Pragmatic Hybrid Persistence: Not every entity needs a historical audit trail.
nodddelets you mix State-Stored aggregates (for simple CRUD entities) and Event-Sourced aggregates (for high-value business logic) in the exact same domain, interacting over the same command bus. - The "Dual-Write" Problem, Solved: Saving to a database and publishing an event usually leads to dropped messages if the server crashes.
nodddesolves this natively with a built-in Transactional Outbox and Unit of Work, ensuring your aggregate state and outgoing events commit in a single ACID transaction. - Bring Your Own ORM: No need to migrate to a niche database.
nodddeprovides production-ready adapters for the tools you already use: Drizzle, Prisma, and TypeORM on top of standard Postgres, MySQL, or SQLite. - Fearless Refactoring: Zero runtime reflection. Because
noddderelies entirely on strict TypeScript inference, if you change a command payload or an event schema, your IDE instantly highlights the exact projections, sagas, and tests that need updating. - Native Observability: Built-in OpenTelemetry instrumentation with zero required configuration. Install
@opentelemetry/api, register a provider, andnodddeautomatically creates spans for command dispatch, projections, sagas, queries, and UoW commits — with full W3C Trace Context propagation through the event store. Works with any backend (Datadog, GCP, Jaeger, etc.).
The TypeScript ecosystem generally forces you to choose between heavyweight OOP frameworks (like NestJS) or committing your entire architecture to Event Sourcing. noddde is built for the pragmatic middle ground. Both noddde and excellent frameworks like Emmett share the exact same modern domain philosophy: we both use pure functions and the Decider pattern to eliminate boilerplate. The difference lies entirely in infrastructure and persistence.
Emmett is designed to be the ultimate developer experience for pure Event Sourced systems (often pairing with EventStoreDB). noddde is designed to bring that same elegant DX to standard relational databases, allowing you to choose your persistence strategy on a per-aggregate basis.
| Feature / Philosophy | NestJS CQRS | Emmett | noddde |
|---|---|---|---|
| Primary Focus | Full application framework modularity. | Dedicated Event Sourcing & Event-Driven systems. | Pragmatic Hybrid DDD & CQRS. |
| Domain Paradigm | Heavy OOP, Base Classes, and @Decorators. |
Pure Functions (Decider Pattern). | Pure Functions (Decider Pattern). |
| Persistence Strategy | Typically State-Stored (via ORMs). | Event-Sourced strictly by default. | Hybrid: Mix State-Stored & Event-Sourced aggregates. |
| Infrastructure Focus | Tightly coupled to the NestJS DI container. | Native append-only Event Stores (e.g., EventStoreDB). | Relational First: Native Drizzle/Prisma + Transactional Outbox. |
| Data Safety | Left entirely to the developer. | Stream Versioning & Optimistic Concurrency. | ACID Unit of Work, Outbox, & Pessimistic Locks. |
| Workflows / Sagas | Stateful classes listening to event buses. | Process Managers reacting to streams. | Pure functions returning commands (executed in UoW). |
| Observability | Manual instrumentation. | Manual instrumentation. | Native OTel: auto spans + W3C Trace Context propagation. |
Not every aggregate requires an audit log. With noddde, you can mix and match. A User profile might just be state-stored (overwriting rows in Postgres), while a Wallet aggregate in the same domain uses full Event Sourcing.
// 1. A State-Stored Aggregate (Just update the state, no events to replay)
const UserProfile = defineAggregate<UserDef>({
// ...
});
// 2. An Event-Sourced Aggregate (Emit events, replay history)
// Same defineAggregate — the persistence strategy is chosen at wiring time
const Wallet = defineAggregate<WalletDef>({
// ...
});
// 3. Wire up with a single persistence adapter — it handles everything
const db = drizzle(connectionString, { schema });
const adapter = new DrizzleAdapter(db);
const myDomain = defineDomain({
writeModel: {
aggregates: { UserProfile, Wallet },
},
readModel: {
projections: {},
},
});
const domain = await wireDomain(myDomain, {
persistenceAdapter: adapter,
aggregates: {
UserProfile: { persistence: "state-stored" },
Wallet: { persistence: "event-sourced" },
},
});An aggregate in noddde is a plain object literal defining initialState, commands, and apply.
import { defineAggregate } from "@noddde/core";
const BankAccount = defineAggregate<BankAccountDef>({
initialState: { balance: 0 },
// Decide handlers decide what happens (Business Logic)
decide: {
Deposit: (command, state) => {
if (command.payload.amount <= 0) throw new Error("Invalid amount");
return {
name: "DepositMade",
payload: { amount: command.payload.amount },
};
},
},
// Evolve handlers evolve the state (Deterministic Replay)
evolve: {
DepositMade: (payload, state) => ({
balance: state.balance + payload.amount,
}),
},
// Type-safe schema evolution for old events
upcasters: bankAccountUpcasters,
});There are no base classes to extend or lifecycle hooks to implement. The evolve handlers are pure and synchronous, making event replay deterministic.
Most frameworks require Sagas (Process Managers) to inject an event bus and manually dispatch commands. In noddde, Sagas are pure functions that return commands as data.
export const OrderFulfillmentSaga = defineSaga<OrderSagaDef>({
initialState: { orderId: "", status: "pending" },
startedBy: ["PaymentCompleted"],
associations: {
PaymentCompleted: (event) => event.payload.orderId,
},
handlers: {
PaymentCompleted: (event, state) => ({
// Update saga state
state: { ...state, status: "awaiting_shipment" },
// Return commands to be dispatched atomically
commands: [
{ name: "ConfirmOrder", targetAggregateId: state.orderId },
{
name: "ArrangeShipment",
targetAggregateId: event.payload.shipmentId,
},
],
}),
},
});The framework wraps the state update and the resulting commands in a single Unit of Work. Testing this requires zero mocking—you simply call the function and assert the returned array.
@noddde/testing provides type-safe test harnesses that express tests in the natural BDD pattern without requiring domain bootstrap or database wiring.
import { testAggregate } from "@noddde/testing";
const result = await testAggregate(BankAccount)
.given(
{ name: "AccountCreated", payload: { id: "acc-1" } },
{ name: "DepositMade", payload: { amount: 1000 } },
)
.when({
name: "Withdraw",
targetAggregateId: "acc-1",
payload: { amount: 200 },
})
.execute();
expect(result.events[0].name).toBe("WithdrawalMade");
expect(result.state.balance).toBe(800);| Package | Description |
|---|---|
@noddde/core |
Types, interfaces, and definition functions — zero runtime deps |
@noddde/engine |
Runtime engine: domain orchestration and in-memory implementations |
@noddde/cli |
CLI for scaffolding aggregates, projections, sagas, domains, and projects |
@noddde/testing |
Type-safe Given/When/Then test harnesses |
| Package | Peer Dependency | Features |
|---|---|---|
@noddde/drizzle |
drizzle-orm >= 0.30 |
PostgreSQL, MySQL, SQLite; advisory locking; convenience schemas |
@noddde/prisma |
@prisma/client >= 5.0 |
Any Prisma-supported database; advisory locking; built-in models |
@noddde/typeorm |
typeorm >= 0.3 |
PostgreSQL, MySQL, MariaDB, MSSQL, SQLite; built-in entities |
| Package | Peer Dependency | Features |
|---|---|---|
@noddde/rabbitmq |
amqplib >= 0.10 |
Exchange-based routing, durable queues, exponential backoff reconnection |
@noddde/nats |
nats >= 2.0 |
JetStream durable consumers, subject-based routing, consumer groups |
@noddde/kafka |
kafkajs >= 2.0 |
Consumer group fan-out, partition key strategy, manual offset commits |
All event bus adapters provide at-least-once delivery with manual acknowledgment and configurable retry policies.
yarn add @noddde/core @noddde/engine
yarn add --dev @noddde/testingHead to the Quick Start Guide to build your first domain, or explore our production-ready sample applications:
| Sample Domain | ORM + Database | Event Bus | Concepts Demonstrated |
|---|---|---|---|
| Hotel Booking | Drizzle + PostgreSQL | @noddde/rabbitmq |
Full-stack: 3 Aggregates, Sagas, Projections, Metadata, HTTP API |
| Auction | Prisma + SQLite | In-memory | Event Upcasting, Projections + ViewStore, CQRS Queries |
| Flash Sale | TypeORM + PostgreSQL | @noddde/nats |
Optimistic & Pessimistic Concurrency, Snapshots, Idempotency |
noddde is built using a strict spec-driven development pipeline. If you want to contribute, please read our CLAUDE.md and specs/README.md to understand how we maintain architectural rigor.
License: MIT | Inspired by the Decider pattern.