Domain-Driven Design: Bounded Contexts, Aggregates, and Context Maps
Learn strategic and tactical Domain-Driven Design, including ubiquitous language, bounded contexts, context mapping, entities, value objects, aggregates, repositories, and domain events.
Why Domain-Driven Design?
Domain-Driven Design (DDD) is an approach for building software around the business domain instead of around tables, screens, or technical layers. It is most useful when the domain is complex, language matters, and different parts of the business use similar words with different meanings.
Key idea: DDD is not primarily about folder structure. It is about making the model reflect the business language and boundaries.
Strategic vs Tactical DDD
DDD has two major levels.
| Level | Focus | Examples |
|---|---|---|
| Strategic DDD | System boundaries and team alignment | Bounded contexts, context maps, subdomains |
| Tactical DDD | Code-level domain model patterns | Entities, value objects, aggregates, repositories |
Use strategic DDD before tactical DDD. Clean entities do not help much if the service boundaries are wrong.
Ubiquitous Language
Ubiquitous language means engineers and domain experts use the same precise language in conversations, diagrams, tests, code, and APIs.
Example: E-Commerce
| Word | Possible Meaning |
|---|---|
| Customer | A registered buyer, a support contact, or a billing account |
| Order | Shopping cart checkout, payment request, or warehouse fulfillment job |
| Product | Catalog item, sellable SKU, or inventory unit |
| Shipment | Carrier label, package, or delivery promise |
DDD forces the team to clarify these meanings.
Ambiguous language creates hidden coupling: If "order" means five things in one codebase, changes become risky because no one knows which meaning a method or table represents.
Bounded Contexts
A bounded context is a boundary inside which a model has one clear meaning. The same term may mean something different in another bounded context.
Bounded Context Heuristics
| Signal | What It Suggests |
|---|---|
| Different teams use different language | Separate contexts |
| Data changes for different reasons | Separate contexts |
| Different scaling or compliance needs | Separate contexts |
| Same entity has conflicting rules | Separate contexts |
| Workflows are tightly coupled | Maybe same context |
Bounded contexts often become service boundaries, but they do not have to. A modular monolith can still have excellent bounded contexts.
Subdomains
Subdomains classify parts of the business by importance and differentiation.
| Subdomain Type | Meaning | Engineering Strategy |
|---|---|---|
| Core domain | Differentiates the business | Invest heavily, model carefully |
| Supporting domain | Necessary but not unique | Build with moderate effort |
| Generic domain | Commodity capability | Buy or use standard tooling |
For a food delivery company, dispatch optimization might be core, restaurant onboarding might be supporting, and payroll might be generic.
Context Mapping
Context maps describe relationships between bounded contexts. This matters because no context is isolated forever.
Common Context Relationships
| Relationship | Meaning |
|---|---|
| Partnership | Two teams coordinate closely and evolve together |
| Customer-supplier | Downstream team depends on upstream team commitments |
| Conformist | Downstream accepts upstream model as-is |
| Anti-corruption layer | Translate another model to protect your own |
| Shared kernel | Small shared model maintained jointly |
| Published language | Stable public contract between contexts |
Anti-Corruption Layer
An anti-corruption layer (ACL) protects your domain model from an external or legacy model.
The ACL translates terms, validates assumptions, and prevents legacy concepts from leaking everywhere.
Entities and Value Objects
Entity
An entity has identity that continues over time even when its attributes change.
class Customer {
constructor(
public readonly id: CustomerId,
public email: EmailAddress,
public status: CustomerStatus
) {}
}
Value Object
A value object is defined by its values. It is usually immutable and has no separate identity.
class Money {
constructor(
public readonly amount: number,
public readonly currency: string
) {
if (amount < 0) throw new Error("Money cannot be negative");
}
}
| Concept | Identity | Mutability | Example |
|---|---|---|---|
| Entity | Has stable identity | Can change over time | Customer, Order |
| Value object | No identity beyond values | Usually immutable | Money, Address, DateRange |
Aggregates
An aggregate is a cluster of domain objects that must be consistent together. The aggregate root is the only object external code should reference directly.
Aggregate Rules
- Protect invariants inside the aggregate.
- Only modify children through the aggregate root.
- Keep aggregates small.
- Reference other aggregates by ID, not object graph.
- Use domain events for cross-aggregate coordination.
Example Invariant
class Order {
private lines: OrderLine[] = [];
private status: "draft" | "placed" = "draft";
addLine(productId: string, quantity: number) {
if (this.status !== "draft") {
throw new Error("Cannot change a placed order");
}
if (quantity <= 0) {
throw new Error("Quantity must be positive");
}
this.lines.push({ productId, quantity });
}
place() {
if (this.lines.length === 0) {
throw new Error("Cannot place an empty order");
}
this.status = "placed";
}
}
Repositories, Services, and Domain Events
| Pattern | Responsibility |
|---|---|
| Repository | Load and save aggregates |
| Domain service | Domain logic that does not naturally belong to one entity |
| Application service | Orchestrate use cases and transactions |
| Domain event | Record something important that happened in the domain |
Domain events should describe facts in the domain language: OrderPlaced, PaymentCaptured, ShipmentDelayed.
DDD and Microservices
DDD helps you find good service boundaries, but it does not require microservices.
| Architecture | How DDD Helps |
|---|---|
| Modular monolith | Clear modules and boundaries inside one deployable |
| Microservices | Service boundaries aligned to bounded contexts |
| Event-driven systems | Domain events become integration signals |
| Legacy modernization | Anti-corruption layers reduce model leakage |
Start with boundaries, not deployment units: A well-designed modular monolith is usually better than poorly separated microservices.
Common DDD Mistakes
| Mistake | Problem | Better Approach |
|---|---|---|
| Anemic model | Domain rules live in services and scripts | Put invariants near the data they protect |
| Huge aggregate | Locking and contention increase | Keep aggregate consistency boundary small |
| Generic repository everywhere | Hides domain intent | Use repositories around aggregates |
| One model for all teams | Language conflicts accumulate | Split bounded contexts |
| DDD for CRUD-only apps | Adds ceremony without benefit | Use simpler patterns |
What to Remember for Interviews
- Ubiquitous language: Code should reflect the language of the domain.
- Bounded contexts: A model is valid inside a boundary, not across the whole company.
- Context maps: Define how contexts integrate and depend on each other.
- Aggregates protect invariants: They are consistency boundaries, not just object graphs.
- DDD informs architecture: It helps choose module and service boundaries.
Practice: Model a ride-sharing domain. Identify bounded contexts such as rider, driver, trip, pricing, payment, dispatch, and support. Then draw the context map and decide where anti-corruption layers are needed.