Leer en español · Lire en français · Auf Deutsch lesen
Section 5.7 of 5.8 · Intermediate · 26 min read
Strategic domain-driven design: bounded contexts and context maps
Video lesson
5 min · narrated, with captionsListen only (audio)
Read the transcript
Every style in this chapter needs boundaries. In this lesson you'll learn the most widely used method for finding them in the business itself: strategic domain-driven design.
In a hospital, the word patient means something different to each department. To reception, it's a name, an address and an appointment. To the ward, it's a bed, medication and observations. To the billing office, it's an insurer and an invoice. One giant form serving them all would be huge, and everyone would fight over it. It works better when each department keeps its own simple record, and they agree how to pass information between them.
Domain-driven design, or DDD, was introduced by Eric Evans in 2003. His argument is that the hardest problems in business software are usually not technical, but in understanding the domain. So software is built around a model of the domain, developed with domain experts. Everyone uses a shared vocabulary, the ubiquitous language, in conversation, documents, tests and code. When two groups use the same word with different meanings, you've found evidence of two different models. That's where boundaries begin.
First, divide the business into subdomains, and classify them. The core domain is what makes the business different. For a food-delivery company, it's probably dispatch: matching orders to couriers and predicting arrival times. It deserves your best people and your most careful modelling. A supporting subdomain is specific and necessary, but not a source of advantage, like menu management. It deserves a sound but simpler design. A generic subdomain is a problem many businesses share, like payments or identity. Usually you should buy it, or build a thin layer over an existing solution.
Now the key idea. A bounded context is a boundary within which one model and one language apply consistently. Here, one customer order looks different in three contexts. In Ordering, it's a basket with prices, discounts and a payment status. In Dispatch, it's a delivery job: a pick-up, a drop-off, a ready time and a courier. And in the Kitchen, it's a ticket: dishes, notes, allergens and preparation time. They share only the order ID and the events that pass between them. Each model stays small, because it serves one purpose.
A context map shows the contexts and how they relate. This one is for the food-delivery platform, where a single shared Order class had grown to ninety four fields. The architect ran a two-day event storming workshop, where business experts and engineers put the domain's events on a timeline, and drew this map from it. Dispatch is the core. Ordering and Dispatch are in a partnership: they succeed or fail together, so they plan and release the hand-over jointly. Menu catalogue supplies Ordering as a customer and supplier: Ordering's needs go on Menu's backlog. Towards systems it doesn't control, a context builds an anticorruption layer, which translates so that the foreign model can't leak in. Menu does this for restaurant point-of-sale systems, and Payments for the payment provider. Dispatch, by contrast, is a conformist: it uses the maps provider's model as it is. That's a deliberate choice, because translating it would cost more than it saves. Payments offers one open, documented API to everyone, and order events have a published, versioned format.
Bounded contexts are the best guide to module and service boundaries. Each has its own model, language, data and ideally one owning team. On the platform, each context first becomes a module of a modular monolith, with its own schema. Inside each module, the hexagonal structure of Section five point six keeps the model independent of infrastructure. Dispatch, with very different scaling and release needs, is the first candidate to become a separate service later. Its boundary is already the right shape. That's why many practitioners say: find the contexts first, and decide deployment second. Services that cut across contexts produce a tightly coupled distributed monolith.
The full section covers all eight relationship patterns, how event storming works, and building blocks such as aggregates. Next, in Section five point eight, the last in this chapter, we ask when a monolith is the right answer. See you there.
What you need to know
- Domain-driven design (Eric Evans, 2003) builds software around a model of the business domain developed with domain experts and expressed in a ubiquitous language used in speech and code alike.
- Subdomains are core (where the business competes), supporting (specific but not differentiating) or generic (solved problems); the classification decides where to invest design effort and what to buy.
- A bounded context is a boundary within which one model and one language apply consistently; the same word can safely mean different things in different contexts.
- A context map shows the relationships between contexts: partnership, shared kernel, customer-supplier, conformist, anticorruption layer, open host service, published language and separate ways.
- Event storming (Alberto Brandolini) discovers contexts by putting domain events on a timeline with the people who know the business.
- Aggregates are consistency boundaries changed through a root that enforces invariants; bounded contexts are the best guide to module and service boundaries.
Why domain-driven design belongs in a chapter about structure
Every style in this chapter needs boundaries. A modular monolith needs module boundaries; the microservices of Chapter 6 need service boundaries; even a hexagonal application needs to know what belongs inside the hexagon. Domain-driven design (DDD) is the most widely used method for finding those boundaries in the business domain itself, rather than in technical layers.
Eric Evans introduced it in Domain-Driven Design: Tackling Complexity in the Heart of Software (Addison-Wesley, 2003). His argument is that in most business software the hardest problems are not technical but in understanding the domain: what a policy, a claim, a delivery or a refund really is, and which rules govern it. Software should therefore be built around a deliberately designed model of the domain, developed jointly by developers and domain experts, and that model should be protected from confusion with other models. In 2015 Evans published a short DDD Reference that summarises the patterns and adds a few, such as partnership and big ball of mud as context-map patterns, and domain events as a building block.
DDD has two halves. Strategic design divides a large domain into subdomains and bounded contexts and maps the relationships between them; it is architecture, and it is the main subject here. Tactical design provides building blocks (entities, value objects, aggregates and others) for writing the model inside one context; it is design, and it is covered briefly.
Ubiquitous language
A ubiquitous language is a shared vocabulary, built by developers and domain experts together, that is used consistently in conversation, documents, tests and code. If dispatchers say a courier is "stacked" when carrying two orders from the same restaurant, then the code says stack, not multiOrderAssignment. When a term turns out to be ambiguous or awkward, the team changes the language and the code together.
This sounds like a matter of naming, but it has architectural consequences. Translation between the words of the business and the words of the code is where misunderstandings, and therefore defects, are born. And the moment two groups use the same word with different meanings, you have found evidence of two different models, which is the starting point for bounded contexts.
Subdomains: core, supporting and generic
The business domain as a whole divides into subdomains: areas of activity such as menus, ordering, payments and dispatch. Practitioners classify them into three kinds, building on Evans's distinction between the core domain and generic subdomains; the three-way split with supporting subdomains was popularised by later writers, notably Vaughn Vernon in Implementing Domain-Driven Design (2013).
The core domain is what makes the business different from its competitors and is the reason the software is worth building. For a food-delivery company it is probably dispatch: matching orders to couriers and predicting arrival times. A supporting subdomain is specific to the business and necessary, but not a source of advantage: menu management, courier onboarding. A generic subdomain is a problem many businesses share, with good existing solutions: payments, identity, email and SMS.
The classification is an investment decision. The core deserves the most experienced people, the richest modelling and code you own completely, because that is where better software turns directly into business advantage. Supporting subdomains deserve sound but simpler designs, often a straightforward layered module. Generic subdomains should usually be bought, used as a service or built as thin adapters over open-source components: building your own identity provider is effort spent where you cannot win. The classification also changes over time; a supporting capability can become core when the business strategy changes.
Bounded contexts
A bounded context is an explicit boundary within which one model, and one ubiquitous language, applies consistently. Inside it, "order" has exactly one meaning. Outside it, the same word may mean something else, and that is acceptable, because each model is designed for one purpose. Evans observed that trying to unify the model of a large system into one is neither feasible nor cost-effective; the attempt produces enormous shared classes that serve every purpose badly and that every team must change.
Subdomains and bounded contexts are different things, though often aligned. A subdomain is part of the problem: it exists in the business whether or not software does. A bounded context is part of the solution: a boundary you choose for a model and, usually, for code and data owned by one team. The ideal is one bounded context per subdomain, but legacy systems often have one context spanning several subdomains, and a large subdomain may need several contexts.
One word, three models
Context maps
A context map shows the bounded contexts of a system and the relationships between them. The relationships describe both the technical integration and the power and communication between the teams, which is why context mapping is as much about organisations as about code. It is a direct application of Conway's law: the map shows where the organisation's communication paths and the system's integration paths must line up.
Many relationships are upstream/downstream: the upstream context's decisions affect the downstream one, but not the reverse. The downstream team then has a choice about how to cope, and that choice is what most of the patterns name.
Context map relationship patterns
| Pattern | What it means | Who adapts to whom | Food-delivery example |
|---|---|---|---|
| Partnership | Two teams succeed or fail together, so they plan jointly and evolve their interface together. | Both, by agreement | Ordering and Dispatch release the order-to-delivery hand-over together. |
| Shared kernel | Two contexts share a small, explicitly agreed part of the model and its code; changes need both teams' consent. | Both, carefully | Dispatch and Courier onboarding share a tiny courier-profile library (vehicle type, courier ID). |
| Customer-supplier | The upstream team (supplier) commits to meeting the downstream team's (customer's) needs, which are part of its planning. | Upstream serves downstream | Menu catalogue supplies menus and prices to Ordering, and Ordering's needs go on Menu's backlog. |
| Conformist | The downstream team adopts the upstream model as it is, because it cannot influence it and translation is not worth the cost. | Downstream conforms | Dispatch uses the maps provider's route and location model directly. |
| Anticorruption layer | The downstream team builds a translation layer so that the upstream model cannot leak into its own. | Downstream translates | Menu translates dozens of restaurant point-of-sale formats into its own menu model; Payments isolates the provider's API. |
| Open host service | The upstream context offers a well-defined protocol or API for all consumers, instead of a custom integration for each. | Upstream publishes once | Payments offers one documented payments API to Ordering, refunds and subscriptions. |
| Published language | A documented, shared exchange format, often used with an open host service. | Both use the language | Order events (OrderPlaced, OrderCancelled) have a versioned, documented schema. |
| Separate ways | The contexts are not integrated at all, because the benefit is lower than the cost. | Neither | Courier payroll reporting is done in a separate finance tool fed by a monthly export, not live integration. |
Two of these patterns deserve special attention. An anticorruption layer (ACL) is a set of adapters and translators that a downstream context builds so that a foreign model, often a legacy system or an external provider, cannot leak into its own model. It is the same idea as the driven adapter of Section 5.6, applied between contexts, and it is the default choice for integrating with anything you do not control. An open host service together with a published language is the upstream's way of serving many consumers well: one documented, versioned protocol instead of a custom integration for each. A shared kernel should stay small; a large one turns two contexts back into one with two teams arguing over it, and Evans's 2015 reference adds big ball of mud as the honest label for an area with no clear boundaries at all, to be contained rather than extended.
Event storming: discovering the contexts
Boundaries are discovered with domain experts, not designed alone. Event storming, created by Alberto Brandolini and first described on his blog in 2013, is the most popular workshop format for this. The people who know the business and the people who build the software stand at a long wall of paper and write domain events (things that happened, in the past tense: Order placed, Courier assigned, Food collected) on orange sticky notes, placing them on a timeline.
The group then adds the commands that cause events, the people and external systems involved, the policies that react to events ("whenever an order is placed, find a courier") and, crucially, the hot spots where people disagree or nobody knows. Clusters of events that share a language and a purpose suggest bounded contexts; places where the same word is used differently suggest boundaries; places where a hand-over happens suggest integration points. A big-picture session for a whole business takes a day or two and needs almost no preparation beyond the right people and a lot of wall space.
Tactical patterns in brief
Inside one bounded context, DDD offers building blocks for the model. An entity has an identity that persists while its attributes change: an order is the same order after its status changes. A value object has no identity and is defined only by its values: two Money(12.50, "GBP") objects are interchangeable, and value objects are usually immutable. A domain event records something meaningful that happened in the domain.
An aggregate is a cluster of entities and value objects treated as one unit for changes, with one entity as its root. Outside code may hold references only to the root and must change the aggregate only through it, so the root can enforce the aggregate's invariants (rules that must always hold, such as "no items can be added after an order is placed"). One transaction should change one aggregate; other aggregates are referenced by ID and updated through domain events, which makes aggregates the natural unit of consistency and later of distribution. A repository loads and saves whole aggregates and looks to the domain like a collection; its interface is a driven port in hexagonal terms.
from dataclasses import dataclass, field
from decimal import Decimal
@dataclass(frozen=True)
class Money: # value object: equal if values equal
amount: Decimal
currency: str
def __add__(self, other):
if other.currency != self.currency:
raise ValueError("currency mismatch")
return Money(self.amount + other.amount, self.currency)
def times(self, n: int):
return Money(self.amount * n, self.currency)
@dataclass
class OrderLine: # entity inside the aggregate
dish_id: str
qty: int
price: Money
@dataclass
class Order: # aggregate root
id: str
restaurant_id: str # other aggregates referenced by ID only
lines: list[OrderLine] = field(default_factory=list)
status: str = "draft"
events: list = field(default_factory=list)
def add(self, dish_id, qty, price):
if self.status != "draft":
raise ValueError("order already placed") # invariant
self.lines.append(OrderLine(dish_id, qty, price))
def place(self, minimum: Money):
total = Money(Decimal(0), minimum.currency)
for line in self.lines:
total = total + line.price.times(line.qty)
if total.amount < minimum.amount:
raise ValueError("below restaurant minimum") # invariant
self.status = "placed"
self.events.append({"type": "OrderPlaced", "order_id": self.id, "total": str(total.amount)})The example keeps the aggregate small, references the restaurant by ID rather than holding a restaurant object, and enforces its rules in methods rather than leaving them to callers. A common mistake is the opposite: aggregates so large that every user's change locks the same rows, or domain objects that are only bags of getters and setters while the rules live in service classes (Martin Fowler calls this the anaemic domain model).
Worked example
Worked example: a context map for the food-delivery platform
The platform has grown to about 60 engineers in eight teams, and every change seems to touch the shared Order class, which now has 94 fields. The architect runs a two-day event-storming workshop with product managers, operations staff, restaurant support and engineers, then draws the context map shown below.
| 1. Find the subdomains | From the event timeline: menu management, ordering and checkout, payments, dispatch (matching couriers to orders and predicting arrival times), courier onboarding, customer notifications, restaurant integration. |
|---|---|
| 2. Classify them | Dispatch is the core: faster, cheaper deliveries are what win customers and restaurants. Menu, ordering and courier onboarding are supporting: necessary and specific to this business but not where it competes. Payments and notifications are generic: solved problems, best bought or built thinly over a provider. |
| 3. Draw bounded contexts | Each subdomain gets its own model. "Order" in Ordering (basket, prices, payment status) is split from "delivery job" in Dispatch (pick-up, drop-off, ready time) and "ticket" in the restaurant's kitchen view. The 94-field class becomes three focused models linked by the order ID. |
| 4. Name the relationships | Ordering and Dispatch: partnership. Menu to Ordering: customer-supplier. Payments: open host service with a published language, and an anticorruption layer towards the external provider. Menu: anticorruption layer towards restaurant point-of-sale systems. Dispatch: conformist to the maps API, a deliberate choice because translating its rich model would cost more than it saves. |
| 5. Decide investment | The best engineers and the most careful modelling go to Dispatch. Payments uses a provider through a thin adapter. Notifications uses a bought messaging service. |
| 6. Decide the deployment | The contexts first become modules of the existing application, turning it into a modular monolith (Section 5.3), each with its own schema and public interface. Dispatch, which has very different scaling and release needs, is the first candidate to become a separate service later. |
Classifying Ordering as supporting surprised some stakeholders. The test is not importance (the company cannot work without ordering) but differentiation: would a better ordering model win the business more customers than a better dispatch model?
The conformist relationship with the maps provider is a conscious trade-off, recorded in a decision record, not an accident. If the provider changes or is replaced, Dispatch will feel it.
The context map
Bounded contexts as module and service boundaries
Bounded contexts are the best available guide to where to draw module boundaries in a monolith and service boundaries in a distributed system. A context has its own model, its own language, its own data and ideally one owning team, which are exactly the properties that make a module cohesive and loosely coupled. In a modular monolith each context becomes a module with its own schema; inside it, the hexagonal structure of Section 5.6 keeps the model independent of infrastructure.
If a context later needs to be deployed separately, its boundary is already the right shape for a service, and its relationships on the context map tell you what the integration must look like. This is why many practitioners recommend finding contexts first and deciding deployment second. The reverse also holds: services cut across bounded contexts (an "order service" that every context writes to) produce the tightly coupled distributed monolith discussed in Section 6.3. Note also that a bounded context is not automatically one microservice; a context can be deployed as several services, and several small contexts can share one deployment.
What strategic DDD buys and costs
| Aspect | Effect | Reason |
|---|---|---|
| Modifiability within a context | High | One model, one language and one owner: changes stay local and are easy to reason about. |
| Team autonomy | High | Context boundaries align with team boundaries, so teams can change their models without negotiating every change. |
| Fit between software and business | High | Ubiquitous language and joint modelling reduce translation errors between experts and code. |
| Upfront and ongoing effort | Medium to high | Modelling sessions, domain experts' time, translation layers and contract maintenance all cost effort. |
| Data duplication and integration | Medium cost | Each context keeps its own copy of what it needs, which must be kept in step through events or APIs. |
| Value for simple domains | Low | For CRUD-heavy systems with little logic, tactical DDD in particular adds ceremony with little return. |
Check your understanding
What is the difference between a subdomain and a bounded context?
A subdomain is part of the business problem and exists whether or not software does; a bounded context is a boundary chosen in the solution within which one model applies. Ideally they align, but they need not.
Your team integrates with a legacy billing system whose model is confusing and that you cannot change. Which context-map pattern protects your model?
An anticorruption layer: translators on your side that convert the legacy model into your own terms, so it cannot leak into your context.
Why should one transaction usually change only one aggregate?
Because the aggregate is the unit whose invariants must hold together; changing several at once creates large locks and coupling, whereas other aggregates can be updated through domain events, which also allows them to be distributed later.
Why is payments usually a generic subdomain for a food-delivery company?
Because taking payments is a solved problem shared by many businesses and is not where the company competes, so it is better bought or built thinly over a provider than modelled in depth.
Sources and further reading
Have a question about this section? ArchitectBot, the course’s AI study assistant, answers from the course first.
Ask: “Our 'Customer' table is used by sales, support and billing and nobody dares change it. How would I use bounded contexts to untangle it without a big-bang rewrite?”