Domain-Driven Design
Centering the codebase on a rich model of the business domain rather than technical layers.
Overview
Domain-Driven Design (DDD) is an approach to software development that centres the model on the business domain rather than technical concerns. Eric Evans codified it in "Domain-Driven Design: Tackling Complexity in the Heart of Software" (2003). Key concepts: Ubiquitous Language (shared vocabulary between developers and domain experts), Bounded Contexts, Aggregates, Entities, Value Objects, Domain Events, and Repositories.
Origin
Eric Evans published the "blue book" in 2003 after observing that the most successful projects kept business experts and developers in continuous dialogue and used that shared language directly in the code. Vaughn Vernon's "Implementing Domain-Driven Design" (2013) and "Domain-Driven Design Distilled" (2016) made the patterns more accessible.
Examples
Aggregate root with domain event emission in TypeScript
class DomainEvent {
readonly occurredAt = new Date();
constructor(readonly name: string) {}
}
class OrderPlaced extends DomainEvent {
constructor(readonly orderId: string, readonly customerId: string) {
super('order.placed');
}
}
class Order {
private _events: DomainEvent[] = [];
private _items: OrderItem[] = [];
private _status: 'draft' | 'placed' | 'fulfilled' = 'draft';
constructor(readonly id: string, readonly customerId: string) {}
addItem(item: OrderItem): void {
if (this._status !== 'draft') throw new Error('Cannot modify a placed order');
this._items.push(item);
}
place(): void {
if (this._items.length === 0) throw new Error('Cannot place an empty order');
this._status = 'placed';
this._events.push(new OrderPlaced(this.id, this.customerId));
}
pullEvents(): DomainEvent[] {
const events = [...this._events];
this._events = [];
return events;
}
}The Order aggregate root enforces invariants (no empty orders, no mutation after placement). Domain events are collected inside the aggregate and pulled out by the application layer, which dispatches them after persistence.
Value Object for Money in TypeScript
class Money {
private constructor(
readonly amount: number,
readonly currency: string
) {
if (amount < 0) throw new RangeError('Amount cannot be negative');
if (currency.length !== 3) throw new Error('Currency must be ISO 4217 code');
}
static of(amount: number, currency: string): Money {
return new Money(Math.round(amount * 100) / 100, currency.toUpperCase());
}
add(other: Money): Money {
if (other.currency !== this.currency) throw new Error('Currency mismatch');
return Money.of(this.amount + other.amount, this.currency);
}
multiply(factor: number): Money {
return Money.of(this.amount * factor, this.currency);
}
equals(other: Money): boolean {
return this.amount === other.amount && this.currency === other.currency;
}
toString(): string {
return this.currency + ' ' + this.amount.toFixed(2);
}
}
const price = Money.of(19.99, 'usd');
const tax = price.multiply(0.08);
console.log(price.add(tax).toString()); // USD 21.59Value Objects have no identity (two Money(10, USD) are equal by value), are immutable, and encapsulate domain rules (currency mismatch, negative amounts). Primitive obsession (using raw numbers for money) is a common anti-pattern DDD addresses.
Use Cases
- 01Complex business domains with intricate rules and workflows (insurance, finance, logistics) where the model must reflect expert knowledge accurately
- 02Large systems split into microservices where Bounded Contexts define natural service boundaries
- 03Long-lived systems where maintaining alignment between code and evolving business requirements is the primary challenge
- 04Teams working directly with domain experts where Ubiquitous Language reduces translation loss
When Not to Use
- //CRUD-heavy applications (content management, settings screens) where the domain is thin and the overhead of Aggregates and Repositories is not justified
- //Small projects or prototypes where the design investment outweighs the benefit
- //Teams without access to domain experts; without Ubiquitous Language, DDD becomes an expensive structural pattern without its primary benefit
Technical Notes
- A Bounded Context defines the scope within which a particular domain model applies. The same term (Customer) may mean different things in Sales and Support contexts; DDD makes this explicit rather than forcing a single shared model
- Aggregates define consistency boundaries: all operations within an Aggregate are atomic; cross-Aggregate operations are eventually consistent via domain events. This maps naturally to microservice boundaries
- The Repository pattern provides a collection-like interface to Aggregates, abstracting the persistence mechanism. It returns fully-constructed domain objects, not raw database rows
- Anti-Corruption Layer (ACL) is a DDD pattern for integrating with external systems or legacy code; it translates between the external model and the bounded context's model without contaminating domain objects
More in Types of Programming