FFFF
Skip to content

Latest commit

 

History

603 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

ddd-kit: tactical Domain-Driven Design building blocks for TypeScript

@shirudo/ddd-kit

Tactical Domain-Driven Design building blocks for TypeScript.

@shirudo/ddd-kit supplies the main parts for a domain model. These parts include value objects, entities, aggregates, domain events, and repositories. The package also supplies application handlers, outbox ports, projections, and adapter contract tests.

It is not an application framework. You keep your HTTP layer, database, queue, ORM, and runtime choices. The kit gives your domain model a strong center and clear boundaries around persistence and side effects.

Release candidate: 3.0 (3.0.0-rc, npm dist-tag next). Latest stable release is 2.2.

The public API follows Semantic Versioning. Breaking changes bump the major version and are documented with migration notes in the CHANGELOG.

npm version license

When This Helps

Use this kit when your TypeScript code has domain rules that deserve more than DTOs and service functions:

  • an order can only be confirmed once
  • a booking must stay inside an allowed date range
  • money must never lose precision at a JSON boundary
  • optimistic concurrency conflicts must be handled deliberately
  • domain events must be persisted and dispatched reliably
  • repository adapters must prove they enforce the same contract

The library is intentionally boring at the edges. It does not ship an ORM, a message broker, decorators, a dependency-injection container, or a web framework. Those choices belong to the application.

Installation

pnpm add @shirudo/ddd-kit @shirudo/result @shirudo/base-error

@shirudo/result and @shirudo/base-error are peer dependencies. Install them once in the consuming app.

The package is ESM-only, requires TypeScript 5.9+, and supports Node 22+, Cloudflare Workers, Vercel Edge, Deno, and Bun.

A Small Aggregate

import {
  AggregateRoot,
  DomainError,
  type DomainEvent,
  type Id,
} from "@shirudo/ddd-kit";

type OrderId = Id<"OrderId">;

type OrderState = {
  status: "draft" | "confirmed";
};

type OrderConfirmed = DomainEvent<
  "OrderConfirmed",
  { orderId: OrderId }
>;

type OrderEvent = OrderConfirmed;

class OrderAlreadyConfirmedError extends DomainError<
  "ORDER_ALREADY_CONFIRMED"
> {
  constructor(orderId: OrderId) {
    super({
      code: "ORDER_ALREADY_CONFIRMED",
      message: `Order ${orderId} is already confirmed.`,
    });
  }
}

class Order extends AggregateRoot<OrderState, OrderId, OrderEvent> {
  protected readonly aggregateType = "Order";

  private constructor(id: OrderId, state: OrderState) {
    super(id, state);
  }

  static draft(id: OrderId): Order {
    return new Order(id, { status: "draft" });
  }

  get status(): OrderState["status"] {
    return this.state.status;
  }

  confirm(): void {
    if (this.state.status === "confirmed") {
      throw new OrderAlreadyConfirmedError(this.id);
    }

    this.commit(
      { status: "confirmed" },
      this.createEvent("OrderConfirmed", { orderId: this.id }),
    );
  }
}

const order = Order.draft("order-1" as OrderId);

order.confirm();

order.status; // "confirmed"
order.version; // 1
order.pendingEvents[0]?.type; // "OrderConfirmed"

That example is deliberately small, but it shows the core shape:

  • The aggregate owns the rule.
  • The domain throws an error when an invariant is broken.
  • commit(...) changes the state and records the event together.
  • createEvent(...) captures the immutable domain decision and aggregate source.
  • The application shell adds event identity, recording time, and trace metadata.
  • Persistence stays outside the aggregate.

In production, a repository and withCommit or UnitOfWork persist the state, write the events to an outbox inside the same transaction, and mark the aggregate as persisted after the transaction commits.

What You Get

Domain modeling

  • value objects via vo() and ValueObject<T>
  • exact Money helpers in @shirudo/ddd-kit/money
  • child entities with branded identity
  • state-stored and event-sourced aggregate roots
  • domain events with metadata, schema version, and commit stamps
  • a domain state machine for named lifecycle states

Application boundaries

  • CommandHandler and QueryHandler types
  • in-process CommandBus and QueryBus for modular apps, tests, and edge runtimes
  • a clear error split: domain code throws, command/query boundaries return Result
  • voValidated for collecting field-level validation issues
  • optional HTTP/RFC 9457 presentation helpers

Persistence and delivery

  • repository interfaces for id-based and filtered access
  • a per-operation Identity Map contract
  • optimistic concurrency errors and duplicate-insert errors
  • withCommit for transaction, outbox, event harvest, and post-commit cleanup
  • UnitOfWork for repository registration and enrollment
  • outbox dispatcher, projection, event-store, and snapshot ports
  • contract tests for repository and outbox adapters

What It Does Not Do

The kit does not decide your architecture for you. It gives you hard boundaries where the domain model needs them and stays out of the rest.

  • No ORM adapter is bundled.
  • No queue or broker is required.
  • No global application container is introduced.
  • No query DSL or expression trees: Specification evaluates in memory and is translated explicitly by adapters, never reverse-engineered into SQL.
  • No money rounding, allocation, or FX policy is hidden in the library.
  • No cross-process command bus is pretended to be in-process code.

Those are application decisions. The guides show the recommended seams.

Guide Map

Start with Getting Started if you want the short walkthrough. Read Design Decisions if you want to understand why the kit is shaped this way. Keep Common Mistakes nearby when writing your first adapter or aggregate.

Topic Guide
Value objects and validation helpers Value Objects
Exact money values Money
Child entities and identity Entities
State-stored aggregates Aggregate Roots
Event-sourced aggregates and snapshots Event Sourcing
Domain event shape and factories Domain Events
Named lifecycle states Domain State Machine
Throwing in the domain, returning Result at the boundary Result vs Throw
Commands, queries, and in-process buses CQRS & Buses
Repository contracts and Identity Map Repository
Transaction-scoped repositories Unit of Work
Duplicate-safe commands and inbox handling Command Idempotency
Reliable event harvest and delivery Outbox & Transactions
Read models and projectors Projections
Event schema changes Event Upcasting
Optimistic concurrency Concurrency
Workers, Deno, Bun, and other edge runtimes Edge Runtimes

The generated API reference lives in docs/api.

Examples

Contributing

pnpm typecheck runs the native TypeScript 7 compiler. The typescript development dependency intentionally aliases the official TypeScript 6 compatibility package because TypeDoc and Vite+ Pack's declaration bundler still consume the compiler API, which TypeScript 7.0 does not expose. Keep the two packages side by side until those API-based tools support TypeScript 7.

Tests and package builds run through Vite+ (pnpm test, pnpm build). Biome remains the repository's lint and format policy.

Bug reports, questions, and pull requests are welcome on GitHub. Please open pull requests against main.

License

MIT.

Author

Shirudo: @shi-rudo | npm | repository

About

Tactical Domain-Driven Design building blocks for TypeScript. Value objects, entities, aggregates, domain events, repositories, and application patterns.

Topics

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

0