SuperAPI AI Directory

domain-driven-design

Coding & development

View repository

Updated 10-16 05:48

About this skill

Domain-Driven Design (DDD)

DDD is a software design approach focusing on modeling software to match a domain according to input from that domain's experts. It is essential for tackling high complexity in the heart of software.

When to Use

  • Complex Business Domains: Healthcare, fintech, logistics, and enterprise systems with intricate operational rules and workflows.
  • Monolith Decomposition: Establishing well-defined Bounded Contexts as boundaries for microservices or modular monoliths.
  • Cross-Functional Team Alignment: Creating a shared Ubiquitous Language between domain experts and software engineers.
  • High-Change Core Software: Managing business logic that evolves rapidly without introducing unexpected regressions.

Quick Start

// Aggregate Root
public class Order {
    private OrderId id;
    private Money totalAmount;
    private OrderStatus status;
    private List<OrderItem> items; // Aggregates items

    // Behaviors (Rich Model), not just Getters/Setters
    public void addItem(Product product, int quantity) {
        if (this.status != OrderStatus.DRAFT) {
            throw new DomainException("Cannot modify confirmed order");
        }
        this.items.add(new OrderItem(product, quantity));
        recalculateTotal();
    }

    public void confirm() {
        if (items.isEmpty()) throw new DomainException("Order empty");
        this.status = OrderStatus.CONFIRMED;
        // Raise Domain Event
        DomainEvents.publish(new OrderConfirmed(this.id));
    }
}

Core Concepts

Strategic DDD: Bounded Contexts & Context Mapping

Divides a large organization into autonomous boundaries with distinct terminology and models:

[ Sales Context ] ──(Customer = Buyer)──→ [ Context Map ] ──(Customer = Borrower)──→ [ Underwriting Context ]

Value Objects vs Entities

Entities possess a persistent identity that endures across mutations; Value Objects are immutable and defined entirely by their attributes:

// domain/value-objects/money.vo.ts
export class Money {
  constructor(
    public readonly amount: number,
    public readonly currency: "USD" | "EUR" | "GBP",
  ) {
    if (amount < 0) throw new Error("Amount cannot be negative");
    Object.freeze(this);
  }

  public add(other: Money): Money {
    if (this.currency !== other.currency) throw new Error("Currency mismatch");
    return new Money(this.amount + other.amount, this.currency);
  }

  public equals(other: Money): boolean {
    return this.amount === other.amount && this.currency === other.currency;
  }
}

Aggregate Roots & Transaction Boundaries

The Aggregate Root is the sole gateway through which external callers can interact with inner entities:

// domain/aggregates/invoice.ts
export class Invoice {
  private readonly lineItems: InvoiceLineItem[] = [];

  constructor(
    public readonly id: string,
    public readonly customerId: string,
  ) {}

  public addLineItem(description: string, price: Money): void {
    if (this.lineItems.length >= 100)
      throw new Error("Maximum line items exceeded");
    this.lineItems.push(
      new InvoiceLineItem(crypto.randomUUID(), description, price),
    );
  }

  get total(): Money {
    return this.lineItems.reduce(
      (sum, item) => sum.add(item.price),
      new Money(0, "USD"),
    );
  }
}

Common Patterns

Aggregate Root with Encapsulated Business Invariants

Problem: Direct mutation of entity state bypasses business rules and produces inconsistent data.
Solution: Protect invariants inside Aggregate Roots and emit Domain Events.

// domain/aggregates/bank-account.ts
export class BankAccount {
  private _balance: number = 0;
  private readonly _domainEvents: any[] = [];

  constructor(
    public readonly id: string,
    initialDeposit: number,
  ) {
    if (initialDeposit < 25) throw new Error("Minimum opening balance is $25");
    this._balance = initialDeposit;
  }

  public withdraw(amount: number): void {
    if (amount <= 0) throw new Error("Withdrawal amount must be positive");
    if (this._balance - amount < 0) throw new Error("Insufficient funds");
    this._balance -= amount;
    this._domainEvents.push({
      type: "FUNDS_WITHDRAWN",
      accountId: this.id,
      amount,
    });
  }

  get balance(): number {
    return this._balance;
  }
  pullDomainEvents(): any[] {
    return this._domainEvents.splice(0);
  }
}

Best Practices

Do:

  • Co-Design with Domain Experts: Conduct Event Storming workshops to map domain workflows before writing code.
  • Enforce Invariants Inside the Aggregate: Ensure invalid state can never exist within an entity or aggregate root.
  • Reference Other Aggregates by ID Only: Never hold direct object references to other aggregate roots; use their unique IDs.
  • Make Value Objects Immutable: Guarantee side-effect free equality checks and safe passing across concurrent threads.

Don't:

  • Create massive aggregates: Keep aggregates small; large aggregates cause database lock contention and performance bottlenecks.
  • Let technical database concerns leak into Domain logic: Design domain models for business behavior, not database normalization.
  • Use DDD for generic CRUD contexts: Reserve DDD tactical patterns for the Core Domain where competitive advantage lies.

Troubleshooting

Error Cause Solution
God Class Aggregate knowing too much. Split Aggregates; use Domain Events to coordinate.
Performance Loading huge Aggregates. Lazy load is tricky; prefer smaller Aggregates tailored to invariants.

References