Be Software Architecture Wise
Be Wise Apps

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 captions
Listen 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

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

The word order means different things in three bounded contexts of a food-delivery platform; each context has its own model, linked by an order identifierOrdering contextOrder = basket, prices,discounts, payment statusstates: draft, placed,paid, cancelledDispatch contextDelivery job = pick-up,drop-off, ready time, courierstates: waiting, assigned,collected, deliveredKitchen contextTicket = dishes, notes,allergens, prep timestates: received, cooking,readyShared only: the order ID (A-17) and the events that pass between contextsEach model is simple because it serves one purpose
"Order" means a basket with prices to Ordering, a pick-up and drop-off job to Dispatch and a list of dishes to cook in the Kitchen context; each model stays small because it serves one purpose.

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

PatternWhat it meansWho adapts to whomFood-delivery example
PartnershipTwo teams succeed or fail together, so they plan jointly and evolve their interface together.Both, by agreementOrdering and Dispatch release the order-to-delivery hand-over together.
Shared kernelTwo contexts share a small, explicitly agreed part of the model and its code; changes need both teams' consent.Both, carefullyDispatch and Courier onboarding share a tiny courier-profile library (vehicle type, courier ID).
Customer-supplierThe upstream team (supplier) commits to meeting the downstream team's (customer's) needs, which are part of its planning.Upstream serves downstreamMenu catalogue supplies menus and prices to Ordering, and Ordering's needs go on Menu's backlog.
ConformistThe downstream team adopts the upstream model as it is, because it cannot influence it and translation is not worth the cost.Downstream conformsDispatch uses the maps provider's route and location model directly.
Anticorruption layerThe downstream team builds a translation layer so that the upstream model cannot leak into its own.Downstream translatesMenu translates dozens of restaurant point-of-sale formats into its own menu model; Payments isolates the provider's API.
Open host serviceThe upstream context offers a well-defined protocol or API for all consumers, instead of a custom integration for each.Upstream publishes oncePayments offers one documented payments API to Ordering, refunds and subscriptions.
Published languageA documented, shared exchange format, often used with an open host service.Both use the languageOrder events (OrderPlaced, OrderCancelled) have a versioned, documented schema.
Separate waysThe contexts are not integrated at all, because the benefit is lower than the cost.NeitherCourier 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)})
An Order aggregate: a value object (Money), an entity (OrderLine), the aggregate root enforcing invariants, and a domain event recorded when the order is placed.

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 subdomainsFrom 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 themDispatch 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 contextsEach 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 relationshipsOrdering 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 investmentThe 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 deploymentThe 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

Context map of the food-delivery platform: bounded contexts, external systems and the relationship pattern on each connectionRestaurant POSexternal, many vendorsMaps APIexternalMenu cataloguesupportingOrderingsupportingDispatchcorePaymentsgenericNotificationsgenericCourier onboardingsupportingPayment providerexternalUDACL in MenuUDConformistUDcustomer /supplierPartnershipDUOHS / PLorder events(publishedlanguage)Shared kernelDUACL in PaymentsU = upstream, D = downstreamCoral box: core subdomain; grey box: external systemDashed arrow: asynchronous events
Each line names the relationship pattern; U and D mark upstream and downstream, and the anticorruption layers sit on the downstream side towards systems the platform does not control.

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

AspectEffectReason
Modifiability within a contextHighOne model, one language and one owner: changes stay local and are easy to reason about.
Team autonomyHighContext boundaries align with team boundaries, so teams can change their models without negotiating every change.
Fit between software and businessHighUbiquitous language and joint modelling reduce translation errors between experts and code.
Upfront and ongoing effortMedium to highModelling sessions, domain experts' time, translation layers and contract maintenance all cost effort.
Data duplication and integrationMedium costEach context keeps its own copy of what it needs, which must be kept in step through events or APIs.
Value for simple domainsLowFor 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.

Next · 5.8Choosing a monolithMonolithFirst, the microservice premium and the majestic monolith; when one deployment unit is the right choice, how far a monolith scales, the signs that it is time to split, and a decision guide for the Chapter 5 styles.Continue →

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?”