Skip to content

aumontzey8765/domain-driven-agent

Repository files navigation

domain-driven-agent

Domain-driven design for TypeScript AI agents
Clone → npm installnpm run dev → REST API and business-logic aggregates.

TypeScript License: MIT Node.js


What is this?

domain-driven-agent is a TypeScript library that applies Domain-Driven Design (DDD) ideas to agents and automation: bounded contexts, aggregates, value objects, domain events, and (optional) event sourcing for replay and audit.

Instead of treating behavior as a single prompt plus loose tools, you model commands → aggregate → domain events → event store, with type-safe value objects and explicit invariants.

Good fits

  • Enterprise automation (orders, approvals, ticketing)
  • Financial or compliance workflows that need an audit trail
  • Any system where rules and state must stay consistent as complexity grows

Features

Feature Description
Aggregate agents AggregateAgent applies events and tracks uncommitted work.
Value objects Immutable, validated types (OrderId, Money, …).
Domain events Typed events with occurredAt persisted for replay.
Event sourcing SQLite EventStore: load by replaying history; optimistic concurrency on save.
Sagas / projectors Saga (orchestration hooks) and Projector (read-model style handlers).
REST API Commands and queries over HTTP (see below).

PostgreSQL / EventStoreDB backends are left as extension points (connectionString is reserved on EventStoreOptions).


Requirements

  • Node.js 22+
  • npm (or compatible client)

Installation

cd domain-driven-agent
npm install

Quick start

1. Run the API

npm run dev

The server listens on port 3000 by default (PORT). The SQLite database path defaults to ./events.db (EVENT_STORE_DB_PATH). See .env.example.

2. Call the API

Health

curl http://localhost:3000/health

Create orderorderId must match ORD- plus nine digits (e.g. ORD-123456789). In JSON, amount must be a number (not a string).

curl -X POST http://localhost:3000/orders/ORD-123456789/commands/create \
  -H "Content-Type: application/json" \
  -d '{"customerId":"CUST-42","amount":99.99,"currency":"USD"}'

Approve

curl -X POST http://localhost:3000/orders/ORD-123456789/commands/approve \
  -H "Content-Type: application/json" \
  -d '{"approvedBy":"manager@example.com"}'

Record payment failure

curl -X POST http://localhost:3000/orders/ORD-123456789/commands/fail-payment \
  -H "Content-Type: application/json" \
  -d '{"reason":"card declined"}'

Read state

curl http://localhost:3000/orders/ORD-123456789/state

status is none until an order is created, then pending, approved, or failed.

Order invariants (example aggregate)approve and fail-payment apply only while status is pending (not after approval or failure).

3. Use the framework in code

From this repository (run with tsx or compile with npm run build and run from dist/):

import { EventStore, OrderAgent, OrderId, Money } from "./src/index.js";

const store = new EventStore({ dbPath: "./events.db" });
const orderId = OrderId.create("ORD-123456789");

const agent = await store.load(orderId.value, () => new OrderAgent(orderId));
agent.createOrder("CUST-42", new Money({ amount: 99.99, currency: "USD" }));
agent.approveOrder("manager@example.com");
await store.save(agent);

const again = await store.load(orderId.value, () => new OrderAgent(orderId));
console.log(again.status); // "approved"

If you depend on this package by name elsewhere, import from domain-driven-agent (after npm run build), matching the main in package.json.


Architecture (overview)

┌─────────────────────────────────────────────────────────────┐
│ Domain: value objects, aggregates, domain events            │
└────────────────────────────┬────────────────────────────────┘
                             │
┌────────────────────────────┼────────────────────────────────┐
│ Application: sagas, projectors, repositories                 │
└────────────────────────────┬────────────────────────────────┘
                             │
┌────────────────────────────┼────────────────────────────────┐
│ Infrastructure: EventStore (SQLite), HTTP API              │
└────────────────────────────┴────────────────────────────────┘

Flow: Command → aggregate → domain event(s) → EventStore

Core API (summary)

ValueObject

Abstract base with equals() and toJSON() so nested value objects persist correctly.

DomainEvent

interface DomainEvent {
  readonly type: string;
  readonly occurredAt?: Date;
}

AggregateAgent

  • get id(): string
  • getUncommittedEvents() / clearEvents()
  • loadFromHistory(events) — replay without duplicating uncommitted events
  • protected apply(event) — applies when, increments version, queues uncommitted events unless replaying

Subclasses implement protected when(event) (see OrderAgent) to route to private on<EventType> handlers.

EventStore

new EventStore({ dbPath?: string; connectionString?: string });

await store.load(aggregateId, () => new OrderAgent(orderId));
await store.save(agent);
await store.getEvents(aggregateId);

Saves enforce optimistic concurrency: if the persisted stream advanced since load, save throws.

Saga / Projector

import { Saga, step, type SagaContext } from "./src/framework/Saga.js";
import type { OrderCreated } from "./src/domain/order/events.js";

const ctx: SagaContext = {
  command: async (agentType, commandName, payload) => {
    /* wire to your application / bus */
  },
};

const saga = new Saga("order-fulfillment").on(
  "OrderCreated",
  step(async (event, c) => {
    const e = event as OrderCreated;
    await c.command("PaymentAgent", "chargeCustomer", { orderId: e.orderId });
  }),
);

await saga.dispatch(domainEvent, ctx);
import { Projector } from "./src/framework/Projector.js";

const projector = new Projector("order-projector").on("OrderApproved", async (event) => {
  /* update read model */
});
await projector.project(event);

Project layout

domain-driven-agent/
├── src/
│   ├── framework/          # ValueObject, AggregateAgent, EventStore, Saga, Projector
│   ├── domain/order/       # Example aggregate + events + value objects
│   ├── infrastructure/     # Repositories
│   ├── api/createApp.ts    # Express app factory
│   ├── server.ts           # HTTP entry (production / dev)
│   └── index.ts            # Library exports
├── tests/
│   ├── framework.test.ts   # Saga + Projector
│   ├── integration/        # Event store
│   ├── order.test.ts
│   └── api.test.ts
├── Dockerfile
├── docker-compose.yml
├── .env.example
├── package.json
├── tsconfig.json
└── vitest.config.ts

Docker

Build and run the API with a persisted SQLite volume:

docker compose up --build

The app listens on 3000 and stores events under /data/events.db in the container.


Scripts

Script Purpose
npm run dev tsx watch on src/server.ts
npm run build tscdist/
npm start node dist/server.js (run npm run build first)
npm test Vitest — unit + API + integration
npm run test:integration Event store integration tests
npm run test:saga Saga tests (tests/framework.test.ts, name filter Saga)

Testing aggregates (example)

import { OrderAgent, OrderId, Money } from "./src/domain/order/index.js";

const order = new OrderAgent(OrderId.create("ORD-123456789"));
order.createOrder("CUST-1", new Money({ amount: 100, currency: "USD" }));
order.approveOrder("manager");
// order.status === "approved"
// order.getUncommittedEvents().length === 2

Contributing

  1. Fork / branch
  2. Add or extend domain tests for new aggregates and invariants
  3. Run npm test and npm run build before submitting changes

Resources


License

MIT

About

TypeScript DDD framework (Domain-Driven Design) for AI agents — aggregates, value objects, event sourcing (SQLite), REST API.

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors