Skills

Install

$ npx ai-agents-skills add --skill domain-driven-design
Domain v1.0

Domain-Driven Design (DDD)

Builds software that closely models complex business domains through shared language between developers and domain experts. Apply to complex business logic; overkill for simple CRUD.

When to Use

  • Complex business rules with many domain interactions
  • Multiple teams working on different business areas
  • Codebase has concepts that mean different things in different contexts
  • Long-lived projects where domain knowledge is central

Don’t use for:

  • Simple CRUD without real business logic
  • Small services (<200 LOC)
  • Tight deadlines with no team DDD experience

Critical Patterns

✅ REQUIRED: Ubiquitous Language

Use domain terms in code, docs, and conversations. Eliminate technical jargon from domain model.

// ❌ WRONG: Technical terms
class Record { process() {} }

// ✅ CORRECT: Business terms
class Order { confirm() {} cancel() {} }  // "confirm" is what the business calls it

✅ REQUIRED: Bounded Context

Explicit boundary within which a model is valid. Same word can mean different things in different contexts.

Sales Context:    Product { name, price, description }
Inventory Context: Product { sku, quantity, location }
Shipping Context:  Package { trackingNumber, weight, dimensions }

Don’t force a single Product model across all contexts. Each context has its own model.

✅ REQUIRED: Entity

Object with identity that persists over time. Two entities with the same attributes are NOT the same entity if their IDs differ.

  • Has a unique, stable identity (never changes)
  • Mutable — attributes can change across its lifecycle
  • Equality is identity-based, not attribute-based
class User {                           // Entity
  constructor(
    readonly id: UserId,               // identity — never changes
    private email: Email,              // state — can change
    private name: string,
  ) {}

  changeEmail(newEmail: Email): void { this.email = newEmail; }

  equals(other: User): boolean {
    return this.id.equals(other.id);   // same id = same user
  }
}

// Two users with same email but different id → NOT the same entity
// Two Money objects with same amount/currency → ARE equal (Value Object)

Entity vs Value Object: Entity = has lifecycle and identity (User, Order, Product). Value Object = defined entirely by its attributes (Money, Email, Address).

✅ REQUIRED: Aggregate + Aggregate Root

Cluster of objects treated as a unit. Only access internals through the Aggregate Root.

class Order {  // Aggregate Root
  private items: OrderItem[];  // Only accessible via Order
  addItem(item: OrderItemDTO): void { this.items.push(new OrderItem(item)); }
  removeItem(itemId: string): void  { this.items = this.items.filter(i => i.id !== itemId); }
}
// ❌ Never: orderItem.save() — always go through Order

✅ REQUIRED: Value Objects

Objects defined entirely by their attributes. No identity, no mutable state. Two Value Objects with the same attributes are always equal and interchangeable.

Key characteristics: immutable (return new instance to “change”), self-validating (throw in constructor), side-effect-free operations, conceptual whole (Money = amount + currency, never just a number).

class Money {
  constructor(readonly amount: number, readonly currency: string) {
    if (amount < 0) throw new Error("Amount cannot be negative");
  }
  add(other: Money): Money {
    if (other.currency !== this.currency) throw new Error("Currency mismatch");
    return new Money(this.amount + other.amount, this.currency);
  }
}

✅ REQUIRED: Domain Events

Capture significant domain occurrences. Decouple side effects from domain logic.

class OrderConfirmedEvent {
  constructor(readonly orderId: string, readonly confirmedAt: Date) {}
}

class Order {
  confirm(): OrderConfirmedEvent {
    this._status = "confirmed";
    return new OrderConfirmedEvent(this.id, new Date());
  }
}

✅ REQUIRED: Repository — Abstract Persistence

Interface that hides database details from the domain. Domain only knows about the interface; infrastructure implements it.

// Domain layer: interface only
interface OrderRepository {
  findById(id: string): Promise<Order | null>;
  save(order: Order): Promise<void>;
  delete(id: string): Promise<void>;
}

// Infrastructure layer: concrete implementation
class PostgresOrderRepository implements OrderRepository {
  async findById(id: string) { /* SQL query */ }
  async save(order: Order)   { /* SQL insert/update */ }
}

✅ REQUIRED: Domain Service — Cross-Aggregate Logic

Stateless service for business logic that doesn’t naturally belong to a single entity or value object.

// ✅ CORRECT: logic spans multiple aggregates → Domain Service
class PricingService {
  calculate(order: Order, customer: Customer): Money {
    const base = order.totalPrice();
    const discount = customer.loyaltyDiscount();
    return base.subtract(discount);
  }
}

// ❌ WRONG: putting cross-aggregate logic inside an aggregate
class Order {
  calculateWithCustomer(customer: Customer) { /* Order shouldn't know Customer */ }
}

❌ NEVER: Anemic Domain Model

Objects that are only data containers with no behavior. Moves business logic to services, destroying the domain model.

// ❌ WRONG: anemic — only data, no behavior
class Order {
  id: string; items: OrderItem[]; status: string;
  // No methods. Business logic lives in OrderService.
}
class OrderService {
  confirm(order: Order) { order.status = "confirmed"; } // leaking business rules
}

// ✅ CORRECT: rich domain model — behavior lives in the entity
class Order {
  confirm(): void {
    if (this.status !== "pending") throw new Error("Only pending orders can be confirmed");
    this.status = "confirmed";
  }
}

Decision Tree

Complex business rules?                → Apply DDD Aggregates + Entities
Multiple teams on different areas?     → Define Bounded Contexts with explicit APIs
Technical jargon in domain model?      → Build Ubiquitous Language with domain experts
Has lifecycle and identity (User, Order)? → Entity (mutable, identity-based equality)
Defined entirely by attributes (Money, Email)? → Value Object (immutable, value equality)
Side effects from domain events?       → Use Domain Events to decouple
Need to persist an aggregate?          → Define a Repository interface
Logic spans multiple aggregates?       → Extract to a Domain Service
Integrating legacy system or 3rd party? → Anti-Corruption Layer (see advanced-patterns.md)
Long-running multi-aggregate workflow? → Saga / Process Manager (see advanced-patterns.md)
Simple CRUD?                           → Skip DDD, not worth the complexity

Example

Order aggregate with value objects, a domain event, and a repository interface.

// Value Object — immutable, defined by value, enforces business rules
class Money {
  constructor(readonly amount: number, readonly currency: string) {
    if (amount < 0) throw new Error("Amount cannot be negative");
  }
  add(other: Money): Money {
    if (other.currency !== this.currency) throw new Error("Currency mismatch");
    return new Money(this.amount + other.amount, this.currency);
  }
}

// Domain Event — captures a significant occurrence
class OrderConfirmedEvent {
  constructor(readonly orderId: string, readonly total: Money, readonly confirmedAt: Date) {}
}

// Aggregate Root — enforces invariants, only entry point to OrderItems
class Order {
  private items: OrderItem[] = [];
  private _status: "pending" | "confirmed" = "pending";

  addItem(sku: string, price: Money, qty: number): void {
    if (this._status !== "pending") throw new Error("Cannot modify confirmed order");
    this.items.push(new OrderItem(sku, price, qty));
  }

  confirm(): OrderConfirmedEvent {
    if (this.items.length === 0) throw new Error("Cannot confirm empty order");
    this._status = "confirmed";
    return new OrderConfirmedEvent(this.id, this.totalPrice(), new Date());
  }

  totalPrice(): Money { return this.items.reduce((sum, i) => sum.add(i.subtotal()), new Money(0, "USD")); }
}

// Repository interface in domain layer — no DB knowledge here
interface OrderRepository { save(order: Order): Promise<void>; findById(id: string): Promise<Order | null>; }

Patterns applied: value object (Money), aggregate root (Order) protecting invariants, domain event (OrderConfirmedEvent), repository interface (infrastructure implements it).


Edge Cases

Aggregate size: Too-large aggregates cause contention (everything locks on Order). Too-small aggregates lose invariant protection. Design around business transactions, not data.

Context boundaries vs microservices: Bounded Contexts are logical, not necessarily microservice boundaries. One service can contain multiple contexts; one context can span services.

DDD without OOP: DDD applies to functional code too. Bounded contexts = modules; aggregates = immutable records with pure functions; domain events = typed messages.

Ubiquitous Language drift: Language agreed at project start diverges over time as business evolves. Regularly revisit with domain experts and update code to match.


Resources

Tactical pattern references:

  • entity.md — Identity-based equality, lifecycle, Entity vs Value Object
  • value-objects.md — Immutability, self-validation, equality contract, conceptual whole
  • aggregate.md — Consistency boundaries, invariant enforcement, root access rules
  • domain-events.md — Facts, naming conventions, event bus, eventual consistency
  • repository.md — Persistence abstraction, interface vs implementation, unit of work
  • domain-service.md — Cross-aggregate logic, stateless services, when to extract
  • advanced-patterns.md — Anti-Corruption Layer, Sagas, Context Mapping