Domain-Driven Design (DDD) — Rules
1. Philosophy
- Domain first — Business logic drives design. Technology follows.
- Ubiquitous language — Shared vocabulary between domain experts and developers.
- Bounded contexts — Explicit boundaries. No shared models across contexts.
- Strategic before tactical — Context mapping before entities.
- Model reflects reality — Code reads like domain specification.
2. Ubiquitous Language
- Glossary — Maintain living document: term → definition → context
- Code speaks language —
Order.place(), notOrderService.create() - Events are facts —
OrderPlaced, notOrderCreatedEvent - No technical jargon in domain — no
DTO,Entity,Repositoryin domain layer
3. Bounded Contexts
Context Map
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Catalog │────▶│ Orders │◀───│ Shipping │
│ Context │ │ Context │ │ Context │
└─────────────┘ └─────────────┘ └─────────────┘
│ │ │
▼ ▼ ▼
Product, Order, Shipment,
Category Customer Tracking
Integration Patterns
| Pattern | When | Coupling |
|---|---|---|
| Shared Kernel | Common subset, tight collaboration | High |
| Customer/Supplier | One drives, other follows | Medium |
| Conformist | Downstream adapts to upstream | Low |
| Anti-Corruption Layer | Legacy/external systems | Low |
| Separate Ways | No relationship | None |
Rules
- Explicit boundaries — separate packages/modules per context
- No shared database — each context owns its data
- Events for integration — domain events, not direct calls
- Context map documented — living diagram
4. Tactical Building Blocks
Entities
// domain/order/Order.ts
export class Order {
private constructor(
public readonly id: OrderId,
public readonly customerId: CustomerId,
private items: OrderItem[],
private status: OrderStatus,
private readonly placedAt: Date,
) {}
static place(customerId: CustomerId, items: OrderItem[]): Order {
if (items.length === 0) throw new Error("Order must have items");
return new Order(
OrderId.generate(),
customerId,
items,
"pending",
new Date(),
);
}
addItem(item: OrderItem): void {
this.ensureEditable();
this.items.push(item);
}
cancel(): void {
this.ensureEditable();
this.status = "cancelled";
DomainEvents.raise(new OrderCancelled(this.id));
}
private ensureEditable(): void {
if (this.status !== "pending") throw new Error("Order not editable");
}
}
Value Objects
// domain/shared/Email.ts
export class Email {
private constructor(public readonly value: string) {}
static create(value: string): Email {
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
throw new Error("Invalid email format");
}
return new Email(value.toLowerCase());
}
equals(other: Email): boolean {
return this.value === other.value;
}
}
// Usage
const email = Email.create("user@example.com");
Aggregates
// Aggregate Root = Entity that controls access to children
export class Order {
// ... entity code
// Children only accessible through root
getItems(): ReadonlyArray<OrderItem> {
return [...this.items];
}
// Invariants enforced at aggregate boundary
addItem(item: OrderItem): void {
const existing = this.items.find((i) => i.productId.equals(item.productId));
if (existing) existing.increaseQuantity(item.quantity);
else this.items.push(item);
this.recalculateTotal();
}
}
Rules Tactical Building Blocks
- Entities have identity —
id+equals() - Value objects immutable — no setters,
equals()by value - Aggregate root controls children — no direct child access
- Invariants in aggregate — business rules enforced at boundary
- Factory methods —
Order.place(), notnew Order()
5. Repositories
Interface (Domain Layer)
// domain/order/OrderRepository.ts
export interface OrderRepository {
findById(id: OrderId): Promise<Order | null>;
findByCustomer(customerId: CustomerId): Promise<Order[]>;
save(order: Order): Promise<void>;
delete(id: OrderId): Promise<void>;
}
Implementation (Infrastructure Layer)
// infrastructure/repositories/PrismaOrderRepository.ts
export class PrismaOrderRepository implements OrderRepository {
constructor(private prisma: PrismaClient) {}
async findById(id: OrderId): Promise<Order | null> {
const data = await this.prisma.order.findUnique({
where: { id: id.value },
});
return data ? this.toDomain(data) : null;
}
async save(order: Order): Promise<void> {
await this.prisma.order.upsert({
where: { id: order.id.value },
create: this.toPersistence(order),
update: this.toPersistence(order),
});
}
private toDomain(data: any): Order {
/* ... */
}
private toPersistence(order: Order): any {
/* ... */
}
}
Rules Repositories
- Interface in domain — implementation in infrastructure
- One repository per aggregate root — not per entity
- Return domain objects — not DTOs/ORM models
- Transaction boundary — repository manages unit of work
6. Domain Events
// domain/shared/DomainEvents.ts
type EventHandler = (event: DomainEvent) => Promise<void>;
class DomainEvents {
private static handlers: Map<string, EventHandler[]> = new Map();
static register(eventName: string, handler: EventHandler): void {
const handlers = this.handlers.get(eventName) || [];
handlers.push(handler);
this.handlers.set(eventName, handlers);
}
static async raise(event: DomainEvent): Promise<void> {
const handlers = this.handlers.get(event.constructor.name) || [];
await Promise.all(handlers.map((h) => h(event)));
}
}
// Event definition
export class OrderPlaced implements DomainEvent {
constructor(
public readonly orderId: OrderId,
public readonly customerId: CustomerId,
public readonly total: Money,
public readonly occurredAt: Date = new Date(),
) {}
}
// Handler (in application layer)
DomainEvents.register("OrderPlaced", async (event: OrderPlaced) => {
await emailService.sendConfirmation(event.customerId, event.orderId);
await inventoryService.reserveItems(event.orderId);
});
Rules Domain Events
- Events are facts — past tense, immutable
- Raise in aggregate —
DomainEvents.raise(new OrderPlaced(...)) - Handlers in application layer — not domain layer
- Async, eventually consistent — no blocking
7. CQRS (Basic)
Command (Write)
// application/orders/PlaceOrderCommand.ts
export class PlaceOrderCommand {
constructor(
public readonly customerId: string,
public readonly items: { productId: string; quantity: number }[],
) {}
}
// Handler
export class PlaceOrderHandler {
constructor(
private orders: OrderRepository,
private events: DomainEvents,
) {}
async handle(cmd: PlaceOrderCommand): Promise<OrderId> {
const customerId = new CustomerId(cmd.customerId);
const items = cmd.items.map(
(i) => new OrderItem(new ProductId(i.productId), i.quantity),
);
const order = Order.place(customerId, items);
await this.orders.save(order);
await this.events.raise(new OrderPlaced(order.id, customerId, order.total));
return order.id;
}
}
Query (Read)
// application/orders/queries/GetOrderQuery.ts
export class GetOrderQuery {
constructor(public readonly orderId: string) {}
}
// Handler (can use read model / projection)
export class GetOrderHandler {
constructor(private readModel: OrderReadModel) {}
async handle(query: GetOrderQuery): Promise<OrderView> {
return this.readModel.findById(query.orderId);
}
}
Rules CQRS
- Commands = writes — return
voidorid - Queries = reads — never mutate
- Separate models — write model (domain) ≠ read model (projection)
- Eventual consistency — read model updated via event handlers
8. Anti-Corruption Layer
// infrastructure/acl/LegacyCustomerAdapter.ts
export class LegacyCustomerAdapter implements CustomerRepository {
constructor(private legacyApi: LegacyApiClient) {}
async findById(id: CustomerId): Promise<Customer | null> {
const dto = await this.legacyApi.getCustomer(id.value);
if (!dto) return null;
return this.toDomain(dto);
}
private toDomain(dto: LegacyCustomerDto): Customer {
return new Customer(
new CustomerId(dto.id),
Email.create(dto.email),
dto.name,
dto.tier as CustomerTier,
);
}
}
Rules Anti-Corruption Layer
- Translate external → domain — no leakage
- Own the mapping — own the translation logic
- Isolate volatility — external changes don't break domain
9. Decision Guide
| Scenario | Pattern |
|---|---|
| Simple CRUD | Skip DDD — use Transaction Script |
| Complex business rules | Entities + Aggregates + Domain Events |
| Multiple teams/contexts | Bounded Contexts + Context Map |
| External integrations | Anti-Corruption Layer |
| High read/write separation | CQRS |
| Audit trail / temporal queries | Event Sourcing |
10. TypeScript Patterns
Branded Types
// types/branded.ts
type Brand<T, B> = T & { __brand: B };
type OrderId = Brand<string, "OrderId">;
type CustomerId = Brand<string, "CustomerId">;
type ProductId = Brand<string, "ProductId">;
// Factory
const OrderId = {
generate: (): OrderId => crypto.randomUUID() as OrderId,
from: (value: string): OrderId => value as OrderId,
};
Result Pattern
type Result<T, E = Error> = { ok: true; value: T } | { ok: false; error: E };
function ok<T>(value: T): Result<T, never> {
return { ok: true, value };
}
function err<E>(error: E): Result<never, E> {
return { ok: false, error };
}
// Usage
async function placeOrder(
cmd: PlaceOrderCommand,
): Promise<Result<OrderId, OrderError>> {
try {
const order = await orderService.place(cmd);
return ok(order.id);
} catch (e) {
return err(new OrderError("Failed to place order", e));
}
}
10. Methodology
Before using ANY DDD pattern not documented in this skill:
- MCP Context7 (priority):
context7_resolve-library-id+context7_query-docsfor DDD, TypeScript. - Official docs: DDD books (Evans, Vernon), TypeScript docs — verify current patterns.
- Project config:
domain/,application/,infrastructure/— verify against actual setup. - HARD RULE: If not in this skill AND cannot be verified against 2 authoritative sources → DO NOT USE IT. Document as assumption or risk in report to orchestrator.
11. Prohibitions
- ❌ Do not use anemic models — logic in entities, not services
- ❌ Do not expose ORM entities as domain objects
- ❌ Do not share aggregates across bounded contexts
- ❌ Do not put business logic in repositories
- ❌ Do not raise events outside aggregates
- ❌ Do not skip invariants — enforce at aggregate boundary
- ❌ Do not use DTOs in domain layer
- ❌ Do not query aggregates — use read models
12. References
Note: For TypeScript rules, see TypeScript Note: For JavaScript conventions, see JavaScript Note: For Repository patterns, see API Design Note: For Event patterns, see API Design
Last updated: 2026-08