正在加载,请稍候…

Domain-Driven Design (DDD) with TypeScript: Practical Implementation

Apply DDD concepts in TypeScript projects. Learn Entities, Value Objects, Aggregates, Domain Services, and Bounded Contexts with practical examples.

Domain-Driven Design (DDD) with TypeScript

DDD focuses on the core domain and domain logic, basing complex design on a model of the domain.

Building Blocks

Value Objects

Immutable objects defined by their attributes, no identity.

class Money {
  private constructor(
    readonly amount: number,
    readonly currency: string
  ) {}

  static of(amount: number, currency: string): Money {
    if (amount < 0) throw new Error('Amount cannot be negative');
    if (!currency || currency.length !== 3) throw new Error('Invalid currency code');
    return new Money(amount, currency);
  }

  add(other: Money): Money {
    if (this.currency !== other.currency) {
      throw new Error('Cannot add different currencies');
    }
    return Money.of(this.amount + other.amount, this.currency);
  }

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

  toString(): string {
    return `${this.amount} ${this.currency}`;
  }
}

Entities

Objects with a unique identity that persists over time.

abstract class Entity<T> {
  constructor(protected readonly id: T) {}

  equals(other: Entity<T>): boolean {
    return this.id === other.id;
  }
}

class Order extends Entity<string> {
  private _status: OrderStatus = 'pending';
  private _items: OrderItem[] = [];

  constructor(
    id: string,
    private readonly customerId: string,
    private readonly createdAt: Date = new Date()
  ) {
    super(id);
  }

  addItem(product: Product, quantity: number): void {
    if (this._status !== 'pending') {
      throw new Error('Cannot modify a non-pending order');
    }
    const existing = this._items.find(i => i.productId === product.id);
    if (existing) {
      existing.increaseQuantity(quantity);
    } else {
      this._items.push(new OrderItem(product.id, product.price, quantity));
    }
  }

  confirm(): void {
    if (this._items.length === 0) throw new Error('Order has no items');
    if (this._status !== 'pending') throw new Error('Order already confirmed');
    this._status = 'confirmed';
  }

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

Aggregates

Clusters of entities/value objects with one root that controls access.

// Order is the aggregate root
class OrderAggregate {
  private events: DomainEvent[] = [];

  static create(customerId: string): OrderAggregate {
    const order = new OrderAggregate(
      generateId(),
      customerId,
      new Date()
    );
    order.addEvent(new OrderCreatedEvent(order.id, customerId));
    return order;
  }

  ship(trackingCode: string): void {
    if (this._status !== 'confirmed') {
      throw new Error('Order must be confirmed before shipping');
    }
    this._status = 'shipped';
    this._trackingCode = trackingCode;
    this.addEvent(new OrderShippedEvent(this.id, trackingCode));
  }

  private addEvent(event: DomainEvent): void {
    this.events.push(event);
  }

  getUncommittedEvents(): DomainEvent[] {
    return [...this.events];
  }

  clearEvents(): void {
    this.events = [];
  }
}

Domain Services

Operations that don't naturally fit within an entity or value object.

class OrderPricingService {
  constructor(
    private discountRepository: DiscountRepository,
    private taxService: TaxService
  ) {}

  async calculateTotal(order: Order, customerId: string): Promise<Money> {
    let total = order.subtotal;

    // Apply customer discounts
    const discounts = await this.discountRepository.findForCustomer(customerId);
    for (const discount of discounts) {
      total = discount.apply(total);
    }

    // Add taxes
    const tax = await this.taxService.calculate(total, order.shippingAddress);
    return total.add(tax);
  }
}

Bounded Contexts

┌─────────────────────┐    ┌─────────────────────┐
│   Order Context     │    │  Inventory Context   │
│  ─────────────────  │    │  ─────────────────── │
│  Order              │    │  Product             │
│  OrderItem          │◄──►│  Stock               │
│  Customer (ref)     │    │  Warehouse           │
└─────────────────────┘    └─────────────────────┘
         │
         ▼
┌─────────────────────┐
│  Payment Context    │
│  ─────────────────  │
│  Payment            │
│  Invoice            │
│  Customer (ref)     │
└─────────────────────┘

DDD pays off most in complex business domains where the logic is the hard part.