Skip to content

dogganidhal/noddde

Repository files navigation

noddde

Type-Safe Domain Modeling & Event Sourcing for TypeScript

CI codecov npm license

Domain modeling that stays out of your way. Production guarantees that protect your data.

DocumentationGetting StartedArchitecture Specs


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.

Why noDDDe?

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 AggregateRoot or fighting with @CommandHandler() decorators. noddde is 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. noddde lets 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. noddde solves 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. noddde provides 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 noddde relies 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, and noddde automatically 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.).

How does noddde compare to the alternatives?

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.

State-Stored or Event-Sourced: You Decide

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" },
  },
});

The API: Pure Functions, Zero Boilerplate

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.

Sagas: Workflows without Side Effects

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.

Testing: Given / When / Then

@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);

Packages

Core

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

Persistence Adapters

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

Distributed Event Bus Adapters

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.

Getting Started

yarn add @noddde/core @noddde/engine
yarn add --dev @noddde/testing

Head 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

Contributing & Architecture

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.

About

Functional DDD framework for TypeScript — Decider pattern, Event Sourcing & CQRS out of the box

Topics

Resources

License

Contributing

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages