ddd-architecture

majiayu000's avatarfrom majiayu000

Domain-Driven Design and Clean Architecture implementation guide

⭐0stars🔀0forks📁View on GitHub🕐Updated Jan 5, 2026

When & Why to Use This Skill

This Claude skill provides a comprehensive implementation guide for Domain-Driven Design (DDD) and Clean Architecture, specifically optimized for TypeScript environments. It offers structured patterns, boilerplate templates, and best practices for building scalable, maintainable, and decoupled software systems by focusing on core business logic and clear architectural boundaries.

Use Cases

  • Designing complex domain models and aggregate roots to enforce business invariants and consistency boundaries.
  • Implementing Clean Architecture layers to decouple core business logic from infrastructure concerns like databases and external APIs.
  • Creating immutable value objects and domain events to capture significant business facts and state changes.
  • Reviewing existing codebases for architectural compliance and refactoring anemic domain models into rich, behavior-oriented designs.
  • Establishing standardized project structures and repository interfaces for large-scale TypeScript applications.
nameddd-architecture
descriptionDomain-Driven Design and Clean Architecture implementation guide
version1.0.0
authorDDD Boilerplate
tags[ddd, clean-architecture, design-patterns, typescript]

DDD Architecture Skill

This skill provides guidance for implementing Domain-Driven Design (DDD) patterns with Clean Architecture principles in TypeScript projects.

When to Use This Skill

Use this skill when:

  • Implementing domain models and aggregates
  • Designing bounded contexts
  • Creating repositories and domain services
  • Applying tactical DDD patterns
  • Reviewing code for architectural compliance
  • Creating new entities, value objects, or domain events

Domain Layer Patterns

1. Aggregate Root

Purpose: Transaction boundary and consistency enforcement

import { AggregateRoot } from '@shared/domain/AggregateRoot';
import { DomainEvent } from '@shared/domain/DomainEvent';

export class Order extends AggregateRoot<OrderId> {
  private items: OrderItem[];
  private status: OrderStatus;

  private constructor(id: OrderId, items: OrderItem[], status: OrderStatus) {
    super(id);
    this.items = items;
    this.status = status;
  }

  // Factory method - only way to create instances
  static create(customerId: CustomerId, items: OrderItem[]): Order {
    if (items.length === 0) {
      throw new InvalidOrderError('Order must have at least one item');
    }
    
    const order = new Order(
      OrderId.generate(),
      items,
      OrderStatus.Pending
    );
    
    // Publish domain event
    order.addDomainEvent(new OrderCreatedEvent(order.id, customerId));
    
    return order;
  }

  // Business method with invariant protection
  confirm(): void {
    if (this.status !== OrderStatus.Pending) {
      throw new InvalidOrderError('Only pending orders can be confirmed');
    }
    
    this.status = OrderStatus.Confirmed;
    this.addDomainEvent(new OrderConfirmedEvent(this.id));
  }

  // Business method
  addItem(item: OrderItem): void {
    if (this.status !== OrderStatus.Pending) {
      throw new InvalidOrderError('Cannot modify non-pending order');
    }
    
    this.items.push(item);
  }

  // Query method
  get totalAmount(): Money {
    return this.items.reduce(
      (total, item) => total.add(item.subtotal),
      Money.zero(Currency.USD)
    );
  }
}

Key Points:

  • Private constructor forces use of factory methods
  • All state changes through business methods
  • Invariants validated before state changes
  • Domain events published for significant changes

2. Value Object

Purpose: Immutable, identity-less domain concepts

import { ValueObject } from '@shared/domain/ValueObject';

export class Money extends ValueObject<{ amount: number; currency: Currency }> {
  private constructor(props: { amount: number; currency: Currency }) {
    super(props);
  }

  static create(amount: number, currency: Currency): Money {
    if (amount < 0) {
      throw new InvalidMoneyError('Amount cannot be negative');
    }
    return new Money({ amount, currency });
  }

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

  get amount(): number {
    return this.props.amount;
  }

  get currency(): Currency {
    return this.props.currency;
  }

  add(other: Money): Money {
    if (!this.props.currency.equals(other.currency)) {
      throw new InvalidMoneyError('Cannot add different currencies');
    }
    return Money.create(this.props.amount + other.amount, this.props.currency);
  }

  multiply(factor: number): Money {
    return Money.create(this.props.amount * factor, this.props.currency);
  }
}

Key Points:

  • Extends ValueObject base class
  • All properties readonly
  • Factory method with validation
  • Returns new instances for operations

3. Entity

Purpose: Objects with identity that can change over time

import { Entity } from '@shared/domain/Entity';

export class OrderItem extends Entity<OrderItemId> {
  private quantity: Quantity;
  private unitPrice: Money;
  private readonly productId: ProductId;

  private constructor(
    id: OrderItemId,
    productId: ProductId,
    quantity: Quantity,
    unitPrice: Money
  ) {
    super(id);
    this.productId = productId;
    this.quantity = quantity;
    this.unitPrice = unitPrice;
  }

  static create(
    productId: ProductId,
    quantity: Quantity,
    unitPrice: Money
  ): OrderItem {
    return new OrderItem(
      OrderItemId.generate(),
      productId,
      quantity,
      unitPrice
    );
  }

  get subtotal(): Money {
    return this.unitPrice.multiply(this.quantity.value);
  }

  updateQuantity(newQuantity: Quantity): void {
    this.quantity = newQuantity;
  }
}

4. Domain Event

Purpose: Record of something significant that happened in the domain

import { DomainEvent } from '@shared/domain/DomainEvent';

export class OrderConfirmedEvent implements DomainEvent {
  readonly occurredAt: Date;
  readonly eventType = 'OrderConfirmed';

  constructor(
    readonly orderId: OrderId,
    readonly customerId: CustomerId,
    readonly totalAmount: Money
  ) {
    this.occurredAt = new Date();
  }
}

Key Points:

  • All properties readonly (immutable)
  • Past tense naming
  • Contains all relevant data
  • Includes timestamp

5. Repository Interface

Purpose: Abstraction for aggregate persistence

// Domain layer - interface only
export interface OrderRepository {
  save(order: Order): Promise<void>;
  findById(id: OrderId): Promise<Order | null>;
  findByCustomerId(customerId: CustomerId): Promise<Order[]>;
  delete(order: Order): Promise<void>;
}

// Infrastructure layer - implementation
export class SqlOrderRepository implements OrderRepository {
  constructor(private readonly db: Database) {}

  async save(order: Order): Promise<void> {
    const model = OrderMapper.toModel(order);
    await this.db.orders.upsert(model);
  }

  async findById(id: OrderId): Promise<Order | null> {
    const model = await this.db.orders.findUnique({
      where: { id: id.value }
    });
    return model ? OrderMapper.toDomain(model) : null;
  }
}

Application Layer Patterns

Use Case / Command Handler

export class CreateOrderUseCase {
  constructor(
    private readonly orderRepository: OrderRepository,
    private readonly productRepository: ProductRepository,
    private readonly eventPublisher: EventPublisher
  ) {}

  async execute(command: CreateOrderCommand): Promise<CreateOrderResult> {
    // 1. Validate and create value objects
    const customerId = CustomerId.create(command.customerId);
    
    // 2. Load necessary aggregates
    const items = await Promise.all(
      command.items.map(async (item) => {
        const product = await this.productRepository.findById(
          ProductId.create(item.productId)
        );
        if (!product) {
          throw new ProductNotFoundError(item.productId);
        }
        return OrderItem.create(
          product.id,
          Quantity.create(item.quantity),
          product.price
        );
      })
    );

    // 3. Execute domain logic
    const order = Order.create(customerId, items);

    // 4. Persist
    await this.orderRepository.save(order);

    // 5. Publish events
    for (const event of order.domainEvents) {
      await this.eventPublisher.publish(event);
    }

    // 6. Return result
    return new CreateOrderResult(order.id.value);
  }
}

DO's and DON'Ts

DO

  • ✅ Start with domain modeling before technical concerns
  • ✅ Use ubiquitous language consistently
  • ✅ Keep aggregates small and focused
  • ✅ Validate invariants in domain objects
  • ✅ Use factory methods for object creation
  • ✅ Publish domain events for state changes
  • ✅ Write unit tests for domain logic without mocks
  • ✅ Use dependency injection for infrastructure

DON'T

  • ❌ Create anemic domain models (data without behavior)
  • ❌ Let ORM/database concerns leak into domain
  • ❌ Modify multiple aggregates in one transaction
  • ❌ Import infrastructure in domain layer
  • ❌ Use primitive types for domain concepts
  • ❌ Skip validation in value objects
  • ❌ Return database models from repositories

File Structure Template

When creating a new bounded context:

src/[context]/
├── domain/
│   ├── [Aggregate].ts          # Aggregate root
│   ├── [Entity].ts             # Entities
│   ├── [ValueObject].ts        # Value objects  
│   ├── [Repository].ts         # Repository interface
│   ├── [DomainService].ts      # Domain services
│   └── events/
│       └── [Event]Event.ts     # Domain events
├── application/
│   ├── commands/
│   │   └── [Action][Entity]Command.ts
│   ├── queries/
│   │   └── Get[Entity]Query.ts
│   ├── handlers/
│   │   ├── [Action][Entity]Handler.ts
│   │   └── Get[Entity]Handler.ts
│   └── dtos/
│       └── [Entity]Dto.ts
├── infrastructure/
│   ├── [Repository]Impl.ts     # Repository implementation
│   ├── mappers/
│   │   └── [Entity]Mapper.ts   # Domain <-> Model mapping
│   └── models/
│       └── [Entity]Model.ts    # Database model
└── interface/
    └── [Entity]Controller.ts   # API endpoints

Common Patterns Reference

Specification Pattern

interface Specification<T> {
  isSatisfiedBy(entity: T): boolean;
}

class OrderEligibleForDiscount implements Specification<Order> {
  isSatisfiedBy(order: Order): boolean {
    return order.totalAmount.amount > 100;
  }
}

Domain Service

class PricingService {
  calculateDiscount(order: Order, customer: Customer): Money {
    // Complex logic that doesn't belong to a single aggregate
    if (customer.isVip && order.totalAmount.amount > 500) {
      return order.totalAmount.multiply(0.1);
    }
    return Money.zero(order.totalAmount.currency);
  }
}

Factory Pattern

class OrderFactory {
  constructor(private readonly productRepository: ProductRepository) {}

  async createFromCart(cart: Cart): Promise<Order> {
    const items = await Promise.all(
      cart.items.map(async (cartItem) => {
        const product = await this.productRepository.findById(cartItem.productId);
        return OrderItem.create(product.id, cartItem.quantity, product.price);
      })
    );
    return Order.create(cart.customerId, items);
  }
}