Lesson 57 of 129
Domain-Driven Design and Bounded Contexts
Domain-driven design (DDD) is an approach to software design that models the software on the business domain, with the help of domain experts. For microservices it provides the main tool for choosing service boundaries: the bounded context. A boundary that follows the language and rules of the business changes less often than one that follows technical layers.
Ubiquitous language
The ubiquitous language is the vocabulary shared by developers and domain experts, used in conversation, documents, and code. Class names, method names, and API fields use the same terms as the business.
The same word often means different things in different parts of a business. "Product" for a catalog team is a description with images and attributes. For the warehouse it is a stock-keeping unit with dimensions and a location. For billing it is a priced item with a tax category. Forcing one model to serve all three produces a large, contradictory class that every team must change.
Bounded context
A bounded context is an explicit boundary within which one model and its ubiquitous language apply consistently. Inside the boundary a term has exactly one meaning. The same real-world thing may appear in several contexts with different models.
graph LR
subgraph Catalog
P1["Product: name, description, images, attributes"]
end
subgraph Inventory
P2["Item: SKU, quantity on hand, warehouse location"]
end
subgraph Billing
P3["Line item: SKU, unit price, tax category"]
end
P1 -.->|"shared identifier: SKU"| P2
P2 -.->|"shared identifier: SKU"| P3Bounded contexts belong to the solution space (the model in the software). They are aligned with, but distinct from, the business's problem space, which is divided into subdomains.
A bounded context is the usual candidate for a service, and a service should not be smaller than one context, since splitting a context splits a single model across a network (see service granularity). One context may contain several deployable components, or a modular monolith may contain several contexts.
Subdomains
A subdomain is a part of the business domain. DDD classifies subdomains by strategic value:
| Type | Definition | Approach |
|---|---|---|
| Core | Gives the business a competitive advantage; complex and changes often | Build in-house with the strongest people and the most careful modeling |
| Supporting | Necessary to the business but not a differentiator; specific to the business | Build simply, or outsource; moderate investment |
| Generic | Solved problem common to many businesses (authentication, payments processing, email) | Buy or use an existing product or open-source component |
For a delivery company, route optimization may be core, customer account management supporting, and payment processing generic. The classification guides where to invest in design and which services warrant dedicated teams. The mapping from subdomains to contexts is ideally one to one, but a legacy system often has one context spanning several subdomains.
Building blocks inside a context
Entities and value objects
- An entity has an identity that persists over time. Two entities with the same attribute values are still different if their identities differ. Example: an
Orderwith an order ID. - A value object has no identity and is defined only by its attributes. It is immutable, and two value objects with equal attributes are interchangeable. Examples:
Money(amount, currency),Address.
Aggregates
An aggregate is a cluster of entities and value objects treated as one unit for data changes. It has one aggregate root, the only entity that outside code may reference and call. The aggregate enforces its invariants (rules that must always hold) inside its boundary.
Rules:
- Outside objects hold references only to the root, by identity.
- A transaction changes one aggregate. Changes across aggregates are made through separate transactions, eventually consistent, typically with domain events (records of a business fact that has happened, such as
OrderConfirmed, published so that other aggregates or services can react). - Aggregates are kept small, since a large aggregate raises contention and transaction size.
import java.util.ArrayList;
import java.util.List;
public class Main {
record Money(long cents) {
Money {
if (cents < 0) {
throw new IllegalArgumentException("cents must not be negative");
}
}
Money times(int quantity) {
return new Money(cents * quantity);
}
Money plus(Money other) {
return new Money(cents + other.cents);
}
}
record OrderLine(String sku, int quantity, Money unitPrice) {}
static final class Order {
private final long id;
private final List<OrderLine> lines = new ArrayList<>();
private boolean confirmed;
Order(long id) {
this.id = id;
}
long id() {
return id;
}
// The root enforces the invariants: no edits after confirmation, quantity > 0.
void addLine(String sku, int quantity, Money unitPrice) {
if (confirmed) {
throw new IllegalStateException("order already confirmed");
}
if (quantity <= 0) {
throw new IllegalArgumentException("quantity must be positive");
}
lines.add(new OrderLine(sku, quantity, unitPrice));
}
void confirm() {
if (lines.isEmpty()) {
throw new IllegalStateException("cannot confirm an empty order");
}
confirmed = true;
}
Money total() {
var sum = new Money(0);
for (var line : lines) {
sum = sum.plus(line.unitPrice().times(line.quantity()));
}
return sum;
}
}
static void attempt(Runnable operation) {
try {
operation.run();
System.out.println("ok");
} catch (IllegalStateException | IllegalArgumentException e) {
System.out.println("rejected: " + e.getMessage());
}
}
public static void main(String[] args) {
var order = new Order(1);
attempt(order::confirm);
order.addLine("A-1", 2, new Money(250));
order.addLine("B-7", 1, new Money(1000));
attempt(() -> order.addLine("C-3", 0, new Money(1)));
order.confirm();
attempt(() -> order.addLine("D-4", 1, new Money(1)));
System.out.println("order " + order.id() + " total " + order.total().cents() + " cents");
}
}rejected: cannot confirm an empty order
rejected: quantity must be positive
rejected: order already confirmed
order 1 total 1500 centsThe root rejects a broken rule with an exception, because a violated invariant is a rule failure that the caller cannot continue past. The invariants live in the root, so no caller can create an inconsistent order. An aggregate is also the natural unit of consistency inside a service: one aggregate maps to one transaction, and other aggregates or other services are updated through events.
Context mapping
A context map documents how bounded contexts relate. In a relationship between two contexts, the upstream context is the one whose model or interface the other depends on, and the downstream context is the dependent one. Changes in the upstream affect the downstream, so the relationship patterns describe who has influence over the interface:
| Pattern | Description |
|---|---|
| Shared kernel | Two contexts share a small part of the model and code; both teams must agree on changes. High coupling; used sparingly |
| Customer-supplier | Upstream context supplies what the downstream needs; downstream can negotiate requirements |
| Conformist | Downstream adopts the upstream model as is, with no influence over it |
| Anti-corruption layer | Downstream translates the upstream model into its own to protect its model (see migrating with the strangler fig) |
| Open host service | Upstream publishes a well-defined protocol for many consumers |
| Published language | A documented shared interchange format (for example, a schema for events) |
| Separate ways | Contexts have no integration because the benefit does not justify the cost |
| Partnership | Two teams coordinate closely and succeed or fail together |
Integration between contexts happens through APIs or events using the published language. Contexts keep their own models and translate at the boundary.
Interview questions
- What is a bounded context? An explicit boundary in which one domain model and one ubiquitous language apply consistently. The same term may have a different model in another context.
- Why is a bounded context a good service boundary? It groups behavior that shares a model and changes for the same business reasons, so most changes stay inside one service and integration happens at well-defined interfaces.
- What distinguishes core, supporting, and generic subdomains? Core subdomains give competitive advantage and warrant in-house investment. Supporting ones are necessary but not differentiating. Generic ones are common solved problems, best bought or reused.
- Why should a transaction modify only one aggregate? The aggregate is the consistency boundary. Limiting transactions to one aggregate keeps them small and avoids contention, and it allows aggregates to live in different services, with eventual consistency between them.
- What is the difference between an entity and a value object? An entity is identified by a persistent identity; a value object is identified only by its attribute values and is immutable.
