Domain-Driven Design: A Practical Introduction
Domain-Driven Design (DDD) is a software development approach popularized by Eric Evans in his seminal 2003 book. It emphasizes aligning software models closely with the business domain — the problem space the software is meant to address. Rather than starting with database schemas or technical architecture, DDD begins with understanding the business itself.
DDD is not a set of technical patterns. It is a philosophy that says the most important part of any software project is the shared understanding between developers and domain experts. When that understanding is strong, the resulting software is more maintainable, flexible, and valuable.
Ubiquitous Language
The cornerstone of DDD is ubiquitous language — a shared vocabulary used by developers, domain experts, product managers, and everyone involved in the project. This language appears in code, documentation, conversations, and specifications.
In traditional development, business experts speak in business terms while developers translate those terms into technical jargon. This translation layer is a source of bugs, misunderstandings, and lost nuance. Ubiquitous language eliminates the translation. If business experts say “order,” the code has an Order class. If they say “cancel,” the method is called cancel().
Building ubiquitous language requires constant refinement. When you discover that “customer” means different things to sales and support, you have found an important distinction. The language should be precise enough that any ambiguity reveals a missing concept.
Building the Language
Start by listening to domain experts. Capture the terms they use naturally. Write them down, define them, and use them consistently in code. When the language changes — and it will — update the code to match. The code becomes an executable version of the business model.
Bounded Contexts
Large organizations use the same terms to mean different things. A “product” in the catalog context has different attributes than a “product” in the shipping context. In DDD, each context gets its own bounded context — a boundary within which a particular model applies.
Each bounded context has its own ubiquitous language, its own data store, and its own team. The boundaries are explicit, and communication between contexts happens through well-defined interfaces.
Context Mapping
Different bounded contexts relate to each other in specific patterns. An upstream/downstream relationship means the upstream context provides data that the downstream consumes. A shared kernel means two contexts share a subset of the model. A conformist relationship means one team adopts the other team’s model without changes.
Identifying your context map is an early strategic DDD exercise. It reveals which teams need to coordinate closely and which can work independently.
Entities and Value Objects
DDD distinguishes two fundamental types of objects: entities and value objects.
Entities
An entity is an object defined by its identity, not its attributes. A person is an entity — even if you change their name, address, or phone number, they remain the same person. In code, entities have a stable identifier (like a database ID) that remains constant throughout their lifecycle.
Entities are mutable. Their attributes change over time, but their identity persists. When you save an entity, you update the existing record rather than creating a new one.
Value Objects
A value object is defined entirely by its attributes. An address is a value object — if the street changes, it is a different address. In code, value objects are immutable. You never change a value object; you create a new one.
Value objects are much simpler than entities. They have no identity, no lifecycle, and no side effects. They can be compared by value. Two value objects with the same attributes are considered equal.
The distinction matters because it changes how you design. Value objects can be freely shared and copied. Entities must be carefully tracked and referenced by identity.
Aggregates
An aggregate is a cluster of related objects treated as a single unit. Each aggregate has a root entity — the aggregate root — that is the only object external code can reference directly.
Consider an order in an e-commerce system. The order is the aggregate root. It contains order lines, shipping details, and payment information. External code can only interact with the order through the root. Order lines cannot be modified directly — you must go through the order.
This rule enforces consistency. The aggregate root is responsible for ensuring that all invariants are maintained. When you add a product to an order, the order validates stock, recalculates totals, and checks business rules — all within its boundary.
Aggregate Design Guidelines
Make aggregates as small as possible while preserving consistency. A common mistake is making aggregates too large, which causes performance problems and contention. Design around transactions — if two objects need to be updated together, they belong in the same aggregate. If they can be updated independently, they belong in different aggregates.
Repositories and Domain Services
Repositories provide the illusion of an in-memory collection of aggregates. They abstract away the underlying storage mechanism. When you need an order, you ask the repository. You do not write SQL or call a database API directly.
Domain services contain business logic that does not naturally fit within an entity or value object. If a calculation involves multiple aggregates or external systems, it belongs in a domain service. Domain services are stateless and named after business activities.
Domain Events
Domain events capture significant occurrences within the domain. They are named in the past tense (OrderShipped, InvoicePaid) and represent something that happened that domain experts care about. Publish domain events from aggregate roots when state changes. Other services within the same bounded context or different bounded contexts react to these events. Domain events enable eventual consistency between aggregates and provide an audit trail of significant business operations.
Anti-Corruption Layer
When integrating with legacy systems or external bounded contexts, an anti-corruption layer translates between models. It converts the external model into your domain model, preventing external concepts from leaking into your domain. This layer typically consists of adapters, facades, and translators that shield your domain from external complexity.
Strategic Design
Beyond tactical patterns, DDD offers strategic design tools for large-scale systems. Subdomains break the business into logical areas — core, supporting, and generic. Core subdomains are what make your business unique and deserve the most investment. Generic subdomains — like authentication or logging — can use off-the-shelf solutions.
The bounded context is also a strategic tool. It defines team boundaries, data ownership, and integration patterns. Aligning team structure with bounded contexts is the principle behind Conway’s Law in reverse: design your system architecture to match your communication patterns.
When to Use DDD
DDD is powerful but not appropriate for every project. It excels in complex domains where business rules are intricate and constantly evolving. Simple CRUD applications gain little from DDD’s overhead. The investment in building a rich domain model pays off when the business logic is complex enough that traditional approaches would produce unmaintainable code.
Start small. Pick one bounded context with rich business logic. Build the ubiquitous language with domain experts. Evolve the model as understanding deepens. DDD is a journey, not a destination — and the journey itself is where the value lies.
FAQ
Do I need to use all DDD patterns for it to work? No. Start with ubiquitous language and bounded contexts. Add entities, value objects, and aggregates as you need them. DDD is a toolbox, not a checklist.
How do I identify bounded contexts? Look for terms that mean different things in different parts of the organization. Different lifecycles, different teams, and different data ownership are also indicators of bounded context boundaries.
What is the difference between a domain service and an application service? A domain service contains business logic that does not naturally fit in an entity or value object. An application service orchestrates the domain layer for a use case — it is part of the application layer, not the domain layer.
How do aggregates relate to database transactions? One aggregate should be loaded and saved in a single transaction. If you need to update multiple aggregates atomically, use domain events and eventual consistency rather than distributed transactions.
Strategic vs Tactical DDD
Domain-Driven Design distinguishes between strategic and tactical patterns. Strategic DDD deals with large-scale structure through Bounded Contexts, Context Maps, and Ubiquitous Language. Tactical DDD provides building blocks — Entities, Value Objects, Aggregates, Repositories, Domain Events, and Domain Services.
Bounded Context
A Bounded Context defines the boundary within which a particular domain model applies. The same concept (e.g., “Customer”) may have different meanings in different contexts — Sales uses “Customer” differently than Support. Each Bounded Context has its own Ubiquitous Language, its own model, and its own codebase. Context Mapping documents the relationships between contexts:
- Partnership — Two contexts collaborate on shared goals
- Shared Kernel — A shared subset of the domain model
- Customer-Supplier — Upstream/downstream relationship
- Conformist — Downstream conforms to upstream model
- Anti-Corruption Layer — Translates between contexts to protect the downstream model
- Open-Host Service — Exposes a public API for other contexts
- Published Language — Well-documented interchange format
- Separate Ways — No integration, completely independent models
Aggregates and Consistency Boundaries
An Aggregate is a cluster of domain objects treated as a single unit. The Aggregate Root is the only entry point — external objects reference only the root, not internal entities. Rules:
- Access Aggregate internals only through the root
- Each transaction modifies only one Aggregate
- Reference other Aggregates by identity, not by object reference
- Eventual consistency is acceptable across Aggregate boundaries
Choosing the right Aggregate size is critical. Overly large Aggregates reduce concurrency and performance. Overly small Aggregates create consistency problems. Start with a conservative boundary and refactor based on transaction volume and consistency requirements.
Related: Clean Architecture | CQRS and Event Sourcing