Domain-Driven Design (DDD)
Status: Complete
Category: Architecture
Default enforcement: Advisory
Author: PushBackLog team
Tags
- Topic: architecture, design
- Skillset: backend, fullstack
- Technology: generic
- Stage: planning, execution, review
Summary
Domain-Driven Design is an approach to software development that centres design on a rich, shared model of the problem domain — co-created by domain experts and engineers. Its core contributions are a set of strategic patterns (bounded contexts, ubiquitous language) and tactical building blocks (aggregates, entities, domain events) that make the implicit structure of complex business domains explicit in code. DDD pays off in domains with genuine complexity and rich business rules; it is overhead in simple CRUD systems.
Rationale
Complexity lives in the domain
The hardest part of most software systems is not the technology — it is understanding the problem domain deeply enough to model it correctly. A system built on an accurate domain model is easier to extend, test, and reason about than one that is technically sophisticated but built on a shallow or incorrect understanding of the business.
DDD places the domain model at the centre of design. Business logic should live in domain objects — not in controllers, not in utility services, not in the database schema — and those objects should use the same language domain experts use. When the code vocabulary drifts from the business vocabulary, the drift signals that the model has become incorrect.
Ubiquitous language closes the translation gap
In most software teams, domain experts and engineers operate in different vocabularies. Domain experts say “subscription renewal” and engineers implement a billing_cycle_trigger with a user_plan_status flag. Every translation introduces ambiguity, and every ambiguity eventually produces a bug or a feature that solves the wrong problem.
DDD’s ubiquitous language eliminates this: one vocabulary, used consistently by domain experts, engineers, product teams, acceptance criteria, and the codebase. If the codebase uses Subscription, Invoice, and Cancellation, those same words appear in every conversation about the domain. The language is owned jointly and updated jointly.
Bounded contexts prevent the big ball of mud
Large systems have multiple sub-domains, each with its own vocabulary and rules. “Customer” means something different in billing than in fulfilment than in support. Forcing a single model to serve all sub-domains produces a model that is technically correct for none.
Bounded contexts are explicit boundaries within which a single model applies. Each context has its own language, its own data model, and ideally its own team ownership. Integration between contexts is modelled explicitly — not through shared database tables or shared class hierarchies. Bounded contexts map naturally to microservices, but they are a design concept that applies equally inside a monolith.
Guidance
Core building blocks
| Concept | Definition | Example |
|---|---|---|
| Entity | Object with continuous identity across state changes | Order with a persistent order ID |
| Value Object | Object defined entirely by its attributes; no identity | Money(100, "GBP") |
| Aggregate | Cluster of entities/value objects with a single consistency boundary | Order containing OrderLine list |
| Aggregate Root | The only entry point for external interaction with an aggregate | Order — never access OrderLine directly |
| Repository | Abstraction for retrieving and persisting aggregates | OrderRepository.findById() |
| Domain Service | Stateless domain logic that spans multiple entities | PricingService.calculateDiscount() |
| Domain Event | An immutable record that something significant occurred in the domain | OrderPlaced, PaymentFailed |
| Bounded Context | An explicit boundary within which a model is consistent | Billing, Fulfilment, Support |
Aggregate design rules
- Reference other aggregates by identity only —
Orderholds acustomerIdstring, not aCustomerobject - Enforce invariants at the aggregate root — all consistency rules for the aggregate are upheld by the root itself
- Keep aggregates small — large aggregates create lock contention and slow transactions
- One transaction per aggregate — if you need to change two aggregates in one transaction, the boundary is probably wrong
// WRONG — bypassing the aggregate root
const item = orderRepository.findItemById(itemId);
item.cancel();
await orderItemRepository.save(item);
// CORRECT — operating through the aggregate root, which enforces invariants
const order = orderRepository.findById(orderId);
order.cancelItem(itemId); // Order validates that cancellation is allowed in current state
await orderRepository.save(order);
Domain events for aggregate coordination
class Order {
private domainEvents: DomainEvent[] = [];
place(customer: Customer, items: OrderItem[]): void {
this.validateCanPlace();
this.status = OrderStatus.Placed;
this.domainEvents.push(new OrderPlaced(this.id, customer.id, this.total));
}
pullDomainEvents(): DomainEvent[] {
const events = [...this.domainEvents];
this.domainEvents = [];
return events;
}
}
Strategic patterns for multi-context systems
| Pattern | When to use |
|---|---|
| Shared Kernel | Two contexts genuinely share a small sub-model; both teams maintain it jointly |
| Customer/Supplier | Downstream context depends on upstream; upstream respects downstream’s reasonable needs |
| Anti-Corruption Layer | Translating between your clean domain model and a messy legacy or external system |
| Open Host Service | Upstream publishes a stable protocol for multiple downstream consumers |
| Separate Ways | Contexts are fully independent; no integration needed |
When DDD is and isn’t appropriate
| Situation | Recommendation |
|---|---|
| Complex domain with rich business rules and expert-dense knowledge | Apply DDD — the investment pays off |
| Multiple teams working on distinctly different sub-domains | Apply bounded contexts at minimum |
| CRUD system with no business rules beyond basic validation | Skip DDD — it adds overhead for no modelling benefit |
| Short-lived prototype or spike | Skip DDD — optimise for speed |
| Legacy system modernisation | Start with an anti-corruption layer; introduce tactical patterns incrementally |
Further reading
- Domain-Driven Design — Eric Evans (the foundational text)
- Implementing Domain-Driven Design — Vaughn Vernon (practical guide)
- Domain-Driven Design Distilled — Vaughn Vernon (concise introduction)