Software Engineering Advanced

Domain-Driven Design

An approach to software development that centres business domain knowledge as the primary driver of design, using bounded contexts, aggregates, and ubiquitous language.

⏱ 12 min read

What it is

Domain-Driven Design (DDD) is a set of principles and patterns for developing software that closely mirrors the business domain it serves. Introduced by Eric Evans in his 2003 book of the same name, DDD divides design concerns into two levels: strategic (how to structure large systems into coherent bounded contexts and manage their relationships) and tactical (how to model the domain within a single context using entities, value objects, aggregates, and domain events).

The central premise is that the most important challenge in complex software is mastering the complexity of the problem domain itself — not technical complexity. DDD addresses this by requiring deep collaboration between developers and domain experts, establishing a ubiquitous language (a shared vocabulary used identically in code, conversations, and documentation), and making the domain model the centrepiece of the design. The result is code that reads like domain documentation and can be evolved by people who understand the business.

Why it exists

Traditional layered architectures often produce anaemic domain models where all business logic migrates into "service" classes, leaving "entity" classes as mere data bags with getters and setters. This leads to a procedural style hidden inside an OOP structure, making it hard to understand where business rules live or how they relate to each other. DDD pushes back by insisting that business logic belongs in the domain model — in entities and value objects that enforce their own invariants.

At scale, different parts of an organisation use the same words to mean different things. In e-commerce, "customer" means one thing to the marketing team (a prospect) and another to the fulfilment team (a shipping address). DDD's bounded contexts acknowledge this reality by explicitly delimiting the scope within which a model is valid, rather than attempting one unified enterprise model that satisfies no domain fully.

When to use

  • Complex business domains with rich, evolving logic that cannot be captured as simple CRUD operations.
  • Systems with multiple teams working on distinct sub-domains that must communicate without tight coupling.
  • Long-lived products where domain knowledge accumulation and knowledge transfer matter.
  • Migration from monolith to microservices — bounded contexts provide natural service boundaries.
  • Domains where incorrect behaviour has high business impact (financial, healthcare, logistics).
  • Teams that include or have access to genuine domain experts willing to engage in design sessions.

When not to use

  • CRUD-heavy applications: If the system is primarily data entry and reporting with minimal business logic, DDD introduces ceremony without value.
  • Early-stage startups: Bounded contexts require stable domain understanding; pivoting frequently undermines the investment in a carefully crafted model.
  • Small teams without domain experts: DDD's collaborative modelling requires willing domain experts; without them, you model assumptions, not domain truth.
  • Greenfield technical infrastructure: Frameworks, libraries, and infrastructure components have no business domain — DDD does not apply.

Typical architecture


STRATEGIC DESIGN — Context Map
───────────────────────────────
  ┌──────────────────┐     Shared Kernel     ┌──────────────────┐
  │   Sales Context  │◄───────────────────►  │  Billing Context │
  │  (Opportunity,   │                        │  (Invoice,       │
  │   Lead, Quote)   │                        │   Payment)       │
  └────────┬─────────┘                        └──────────────────┘
           │  Customer-Supplier (upstream)
           ▼
  ┌──────────────────┐   Anti-Corruption Layer
  │ Fulfilment Context│◄════════════════════  Legacy ERP
  │ (Order, Shipment) │  (translates legacy
  └──────────────────┘    concepts to domain)

TACTICAL DESIGN — Inside a Bounded Context
──────────────────────────────────────────
  Aggregate Root: Order
  ├── Entity: OrderLine (identity within aggregate)
  ├── Value Object: Money (amount + currency, immutable)
  ├── Value Object: Address (immutable shipping address)
  └── Domain Event: OrderShipped

  Repository: OrderRepository (persistence abstraction)
  Domain Service: PricingService (spans multiple aggregates)
  Factory: OrderFactory (complex construction logic)

  Application Layer
  └── OrderApplicationService (orchestrates use cases)
      ├── calls OrderRepository
      ├── calls PricingService
      └── publishes domain events

Pros and cons

Pros

  • Produces code that closely mirrors business reality, making it understandable to domain experts.
  • Bounded contexts provide natural microservice boundaries with well-defined contracts.
  • Aggregates enforce consistency boundaries, preventing invalid state transitions.
  • Ubiquitous language reduces translation errors between business requirements and implementation.
  • Domain events enable loose coupling between contexts and support event sourcing.

Cons

  • High initial investment — event storming sessions and context mapping require significant time.
  • Steep learning curve; tactical patterns (aggregates, repositories, domain services) are easily misapplied.
  • Overkill for simple domains; introduces unnecessary complexity for CRUD-centric applications.
  • Requires sustained collaboration with domain experts, which is often hard to maintain.
  • Anaemic domain model antipattern is easy to fall back into under delivery pressure.

Implementation notes

Aggregates: The most misunderstood tactical pattern. An aggregate is a cluster of entities and value objects with a single entry point — the aggregate root — that enforces all invariants. The rule: you may only hold a reference to an aggregate root, never to internal entities. Aggregates should be designed small — one to four entities — to keep transactions short. "Should order lines be inside the Order aggregate?" Yes, because you cannot have a valid order without lines and the total must be consistent. "Should the customer be inside the Order aggregate?" No — customers exist independently and an order should reference the customer by ID only.

Context mapping: The relationship between bounded contexts must be explicit. A Shared Kernel means two contexts share a subset of the model — changes require coordination. A Customer-Supplier relationship means the upstream (supplier) publishes a model the downstream (customer) consumes; the downstream has a voice but the upstream has veto. An Anti-Corruption Layer (ACL) translates an upstream model (often a legacy system) into the downstream's domain model, preventing domain concepts from being polluted by legacy terminology. Open-Host Service with a Published Language (e.g., a well-documented REST API) allows multiple consumers without individual negotiations.

Common failure modes

  • Anaemic domain model: Entities contain only getters/setters; all logic lives in service classes — the classic DDD failure mode that defeats the purpose of domain modelling.
  • Aggregate designed too large: Including ten entities in one aggregate causes long-running transactions, contention, and eventual consistency problems.
  • Shared database across contexts: Two bounded contexts accessing the same tables violates context isolation and re-creates the global model problem DDD was meant to solve.
  • Ubiquitous language drift: Code uses different terminology from domain experts' conversations — the language diverges over time because documentation and code are not co-maintained.
  • Event storming without follow-through: Doing event storming sessions once and then abandoning the model under sprint pressure; the model must evolve continuously.

Decision checklist

  • Is there a genuine complex domain with non-trivial business rules that justify the DDD investment?
  • Can you identify domain experts who are available and willing to participate in modelling sessions?
  • Have you run an event storming session to discover domain events, commands, and aggregates collaboratively?
  • Are bounded context boundaries drawn around the domain's natural semantic boundaries, not technical ones?
  • Does every aggregate enforce its own invariants without needing to query other aggregates?
  • Is the ubiquitous language reflected in class names, method names, and module names in the code?

Example use cases

  • E-commerce platform: Separate bounded contexts for Catalogue, Inventory, Orders, Fulfilment, and Billing — each with its own model of "product" and "customer".
  • Healthcare system: Patient records, appointments, billing, and clinical trials each represent distinct bounded contexts with strict consistency rules within aggregates.
  • Insurance: Policy underwriting, claims processing, and premium calculation are naturally separate bounded contexts that interact through published events.
  • Microservices — Bounded contexts from DDD are the primary basis for defining microservice boundaries.
  • Event Sourcing — Domain events in DDD map directly to event sourcing; aggregates emit events that are the source of truth.
  • CQRS — Command/Query Responsibility Segregation is a natural complement to DDD aggregates for complex read/write patterns.
  • SOLID Principles — DDD aggregates operationalise SRP (one reason to change) and DIP (domain layer depends on abstractions for persistence).

Further reading