ddd

SKILL.md

Domain-Driven Design

Strategic DDD

┌─────────────────┐     ┌─────────────────┐
│  Order Context   │────▶│ Payment Context  │
│  (core domain)   │     │  (supporting)    │
└────────┬────────┘     └─────────────────┘
┌─────────────────┐     ┌─────────────────┐
│ Inventory Context│     │ Notification Ctx  │
│  (supporting)    │     │  (generic)       │
└─────────────────┘     └─────────────────┘

Tactical DDD (TypeScript)

Entity

class Order {
  private constructor(
    readonly id: OrderId,
    private items: OrderItem[],
    private status: OrderStatus,
    private readonly createdAt: Date,
  ) {}

  static create(items: OrderItem[]): Order {
    if (items.length === 0) throw new DomainError('Order must have at least one item');
    return new Order(OrderId.generate(), items, OrderStatus.PENDING, new Date());
  }

  get total(): Money {
    return this.items.reduce((sum, item) => sum.add(item.subtotal), Money.zero('USD'));
  }

  confirm(): DomainEvent[] {
    if (this.status !== OrderStatus.PENDING) throw new DomainError('Can only confirm pending orders');
    this.status = OrderStatus.CONFIRMED;
    return [new OrderConfirmed(this.id, this.total)];
  }
}

Value Object

class Money {
  private constructor(readonly amount: number, readonly currency: string) {
    if (amount < 0) throw new DomainError('Amount cannot be negative');
  }

  static of(amount: number, currency: string): Money {
    return new Money(Math.round(amount * 100) / 100, currency);
  }

  static zero(currency: string): Money { return new Money(0, currency); }

  add(other: Money): Money {
    if (this.currency !== other.currency) throw new DomainError('Currency mismatch');
    return Money.of(this.amount + other.amount, this.currency);
  }

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

Domain Event

class OrderConfirmed implements DomainEvent {
  readonly occurredAt = new Date();
  constructor(readonly orderId: OrderId, readonly total: Money) {}
}

Repository (Port)

interface OrderRepository {
  findById(id: OrderId): Promise<Order | null>;
  save(order: Order): Promise<void>;
  nextId(): OrderId;
}

Application Service

class ConfirmOrderUseCase {
  constructor(
    private orders: OrderRepository,
    private eventBus: EventBus,
  ) {}

  async execute(orderId: string): Promise<void> {
    const order = await this.orders.findById(OrderId.from(orderId));
    if (!order) throw new NotFoundError('Order', orderId);

    const events = order.confirm();
    await this.orders.save(order);
    await this.eventBus.publishAll(events);
  }
}

DDD Building Blocks

Building Block Purpose Identity? Mutable?
Entity Domain object with identity Yes (ID) Yes
Value Object Immutable descriptor No (structural equality) No
Aggregate Consistency boundary Root entity has ID Yes (via root)
Domain Event Something that happened Event ID No
Repository Persistence abstraction N/A N/A
Domain Service Logic not belonging to entity N/A N/A

Anti-Patterns

Anti-Pattern Fix
Anemic domain model (logic in services) Put business logic in entities
Aggregate too large Keep aggregates small, reference by ID
Exposing entity internals Use methods that express domain intent
Cross-aggregate transactions Use domain events for eventual consistency
Repository returning DTOs Return domain objects, map in application layer

Production Checklist

  • Bounded contexts identified and documented
  • Ubiquitous language in code matches business terms
  • Aggregates enforce invariants
  • Value objects for all descriptors (Money, Email, Address)
  • Domain events for cross-context communication
  • Repository pattern for persistence abstraction
Weekly Installs
13
GitHub Stars
2
First Seen
10 days ago
Installed on
cursor12
gemini-cli12
amp12
cline12
github-copilot12
codex12