DDD strategic design: bounded contexts, ubiquitous language, the anti-corruption layer
What it is
Domain-driven design has two halves and they are usually conflated. Strategic design is about boundaries between models; tactical design is about the objects inside one. Strategic is the half that matters at staff level, because it is the half that determines service boundaries, team boundaries and integration cost.
BOUNDED CONTEXT An explicit boundary within which a
model and its terms have ONE consistent
meaning. Outside it, the same word may
mean something different, and that is
allowed.
UBIQUITOUS LANGUAGE The vocabulary shared between domain
experts and code, WITHIN a bounded
context. Not company-wide.
CONTEXT MAP How contexts relate: which depends on
which, and on what terms.
ANTI-CORRUPTION A translation layer at a boundary that
LAYER (ACL) stops another context's model leaking
into yours.
Commonly confused with a modelling technique for entities. The tactical patterns (aggregates, entities, value objects, repositories) are the part everyone implements and the part that delivers least, and Eric Evans has said publicly that the emphasis on tactical patterns over strategic design was the main misunderstanding of his book.
Also commonly confused with microservices. A bounded context is a model boundary; a service is a deployment boundary. They frequently align and they are not the same decision, and a modular monolith can have perfectly good bounded contexts.
The problem it solves
The word means different things in different parts of the business, and a single shared model forces them into one definition that fits none of them.
"CUSTOMER" in one company:
SALES a lead with a pipeline stage, an owner, a
probability, and a next action. May not have
bought anything.
BILLING a legal entity with a tax ID, a payment
method, and an address that must match the
card. One per invoice stream.
SUPPORT whoever is contacting us, identified by email,
possibly several people at one billing
customer.
SHIPPING an address and a set of delivery
constraints. Might be a different person
entirely.
THE SINGLE-MODEL FAILURE
One Customer class with every field. Sales sets fields
Billing does not understand. A change for Support breaks
Shipping. Nobody can change it without consulting four
teams, so eventually nobody changes it and everyone adds
a side table.
That "God object nobody can change" is the outcome bounded contexts prevent, and the mechanism is simply permitting four Customers with four meanings and being explicit about how they map.
Mechanics
Finding the boundaries
Boundaries are found in language, not in the schema. The signals:
1. THE SAME WORD MEANS DIFFERENT THINGS.
The strongest signal. If "order" means "a signed
contract" to Sales and "a shipment request" to
Fulfilment, that is two contexts.
2. DIFFERENT WORDS FOR THE SAME THING.
Sales says "account", Support says "client", Billing
says "payer". Sometimes the same concept, sometimes
three genuinely different ones, and the conversation
that resolves it is the valuable part.
3. THE MODEL HAS FIELDS ONLY ONE TEAM USES.
A class where half the fields are null for half the
consumers is two models sharing a name.
4. CHANGES CLUSTER.
Which fields change together, and for which team's
reasons? Fields that always change together belong
together.
5. TEAM AND ORGANISATIONAL SEAMS.
Conway's law means the boundaries will end up following
the organisation whether you intend it or not, so it is
better to choose deliberately.
The practical technique: interview the domain experts separately and listen for vocabulary collisions. A word that needs qualifying ("well, the sales customer") is a boundary announcing itself, and that qualification is exactly what a bounded context makes unnecessary.
Ubiquitous language, and the part people get wrong
THE RULE
Within a context, the code uses the domain expert's words,
exactly, and the domain expert uses the code's words.
If the business says "policy lapses", the class is not
called SubscriptionCancellation.
WHAT PEOPLE GET WRONG
Trying to make it COMPANY-WIDE. A single glossary across
the whole organisation is the single-model failure in
documentation form: it forces one definition of
"customer" and every team then quietly means something
else anyway.
The language is ubiquitous WITHIN A CONTEXT. Across
contexts, translation is expected and correct.
The test for whether you have it: can a domain expert read a method name and say whether it is
right? If the code says processTransaction and the expert would say "settle the claim", the
language is not shared, and every conversation between them pays a translation cost forever.
The context map: how contexts relate
Seven relationship patterns, and the useful thing is that they are about power and obligation rather than about technology.
SHARED KERNEL Two contexts share a subset of the
model. Requires tight coordination.
Usually a smell: it means the boundary
is in the wrong place.
CUSTOMER / SUPPLIER Downstream's needs influence upstream's
roadmap. Requires the upstream team to
accept the obligation, which is an
organisational fact rather than a
technical one.
CONFORMIST Downstream adopts upstream's model
wholesale, because it has no influence.
Cheap, and upstream's model leaks
everywhere downstream.
ANTI-CORRUPTION Downstream translates at the boundary.
LAYER Costs a layer, protects the model.
OPEN HOST SERVICE Upstream publishes a deliberately
designed protocol for many consumers,
rather than exposing its internals.
PUBLISHED LANGUAGE A shared interchange format both sides
translate to and from.
SEPARATE WAYS No integration at all. Sometimes the
right answer, and rarely considered.
"Separate ways" is worth naming because it is under-used: two contexts that would need expensive translation and share little are sometimes better duplicating a small amount of data than integrating.
The anti-corruption layer
The pattern that earns its keep most reliably, and the one to reach for when integrating with anything you do not control.
WITHOUT AN ACL
The legacy system's SOAP model, its nullable everything,
its magic status codes and its "CUST_TYPE_3" enums
propagate into your domain objects. Every new feature
works around them. Five years later the legacy system is
retired and you cannot remove its shape from your code.
WITH AN ACL
One translation layer. Your domain sees your model.
When the legacy system is replaced, you change the ACL
and nothing else.
# The ACL is a boundary, and its job is to be the ONLY place
# that knows the other system's vocabulary.
class LegacyBillingACL:
"""Translates the mainframe's model into ours. Nothing
outside this class knows what CUST_TYPE_3 means."""
_TYPE_MAP = {"CUST_TYPE_1": AccountKind.INDIVIDUAL,
"CUST_TYPE_3": AccountKind.ENTERPRISE}
def fetch_account(self, account_id: AccountId) -> Account:
raw = self._soap.GetCustomerRecord(CUSTNO=str(account_id))
return Account(
id=account_id,
kind=self._TYPE_MAP[raw.CUST_TYPE],
# Their system encodes "no credit limit" as -1.
# Our model has an Optional. The translation
# happens HERE and nowhere else.
credit_limit=None if raw.CRED_LIM == -1
else Money(raw.CRED_LIM, "USD"),
status=self._translate_status(raw.STAT_CD),
)
Two properties that make an ACL real rather than nominal:
Nothing outside it uses the foreign vocabulary. If CUST_TYPE_3 appears anywhere else, the ACL
has failed. A lint rule or an architecture test enforcing that is cheap and worth having.
It translates semantics, not just field names. Mapping CRED_LIM to creditLimit is renaming;
mapping -1 to None because their sentinel means "unlimited" is translation. Most ACLs that
fail do so by being a renaming layer.
Bounded contexts and service boundaries
A BOUNDED CONTEXT IS A MODEL BOUNDARY.
A SERVICE IS A DEPLOYMENT BOUNDARY.
They often align, and aligning them is a good default,
because a service that spans two contexts has two models
inside it and will grow a God object.
BUT: a modular monolith can hold several bounded contexts
perfectly well, with module boundaries enforced in the
build. That is frequently the right shape, and it keeps
one transaction boundary and one deploy.
THE RULE I WOULD APPLY
Get the CONTEXT boundaries right first, in a monolith if
possible. Extract to services later, along boundaries you
have already validated by living with them.
Extracting along a boundary you guessed is how you get
distributed transactions between two things that should
have been one.
That ordering is the practical value of strategic DDD: it lets you find the seams cheaply, before the seams become network calls.
A worked example: finding the contexts
AN INSURANCE PRODUCT, one Postgres schema, one Rails app,
40 engineers, and every change requiring three teams.
THE SYMPTOM
A `Policy` model with 94 columns. Underwriting, Claims,
Billing and Renewals all touch it. Nobody can change it.
THE LANGUAGE AUDIT (two days, interviewing each team
separately)
"POLICY" means:
Underwriting a risk assessment in progress, with a
quote, factors and a decision. May never
become a real policy.
Claims a coverage contract with limits,
deductibles and exclusions, as of a date.
Historical versions matter enormously.
Billing a payment schedule with a premium and a
payer. Cares about dates and amounts,
nothing about coverage.
Renewals an expiring contract with a renewal
decision and a new quote.
Four meanings. That is four contexts, and the 94-column
table was four models sharing a name.
THE CONTEXT MAP
Underwriting --(published language: QuoteAccepted event)--> Policy Admin
Policy Admin --(open host: PolicyVersion API)--> Claims
Policy Admin --(open host: PolicyVersion API)--> Billing
Billing --(ACL)--> Legacy Payments Mainframe
Renewals --(customer/supplier)--> Underwriting
WHAT EACH RELATIONSHIP MEANT IN PRACTICE
Published language for Underwriting -> Policy Admin,
because the handoff is an event and both sides agreed a
stable payload.
Open host for Policy Admin, because three consumers
needed policy data and a designed API beats three bespoke
integrations.
ACL to the mainframe, because its model is 30 years old
and must not propagate.
Customer/supplier from Renewals to Underwriting, which
required an explicit agreement that Underwriting would
take Renewals' requirements into its roadmap, and that
was a management conversation rather than a technical one.
WHAT THEY DID
Modules in the monolith first, with boundaries enforced
in the build and each context owning its own tables.
Six months of living with the boundaries, during which
two were moved because the first cut was wrong.
THEN extracted Claims, which had the least chatty
relationship with the others.
WHAT THEY DID NOT DO
Extract four services immediately along boundaries drawn
in a workshop. The two boundaries that moved would have
become distributed transactions.
The finding worth extracting: the four-day language audit found the boundaries, and the six months of living with them found the two that were wrong. Neither would have been discovered by modelling on a whiteboard, and extracting immediately would have made both mistakes permanent.
Production evidence
Eric Evans, Domain-Driven Design (2003), is the source, and part IV (strategic design) is the half that matters most and is read least. Evans has said in later talks that the emphasis on the tactical patterns was the main misreading of the book.
Vaughn Vernon's Implementing Domain-Driven Design (2013) is the more practical treatment and is explicit that context boundaries should be found before service boundaries.
The context-mapping patterns (shared kernel, customer/supplier, conformist, ACL, open host, published language, separate ways) are from Evans and are elaborated in the DDD community's context mapping work, notably Brandolini's.
Alberto Brandolini's Event Storming is the standard workshop technique for finding contexts, and its central insight is that boundaries appear where the language changes, which is why the technique is a facilitated conversation rather than a modelling exercise.
Team Topologies (Skelton and Pais) connects bounded contexts to team boundaries explicitly via cognitive load, and its argument that a team should own a bounded context they can hold in their heads is the organisational half of this.
Segment's and Amazon Prime Video's consolidation write-ups are the cautionary evidence for extracting services along guessed boundaries: both describe operational cost exceeding the isolation benefit when the boundaries were not validated first.
The debate
The case for strategic DDD: it is the only widely-known technique for finding service and team boundaries from something other than the existing schema or the current org chart, and getting those boundaries wrong is the most expensive architecture mistake available.
The case against the ceremony: event storming workshops, context maps and glossaries are substantial effort, and for a small product with one obvious domain the boundaries are apparent without any of it. DDD's vocabulary can also become a way of relabelling ordinary design decisions as insight.
The case for tactical DDD without strategic: aggregates and value objects improve a codebase immediately and require no organisational buy-in.
My position: do the strategic half, skip most of the tactical vocabulary, and find contexts before extracting services.
The strategic half is where the money is, because bounded contexts determine service boundaries and team boundaries, and both are expensive to change. The tactical patterns improve a codebase and are recoverable; a wrong service boundary becomes a distributed transaction and stays for years.
The technique I would actually use is the language audit: interview each team separately and listen for the same word meaning different things. In the insurance example that took two days and found four contexts inside a 94-column table, and no amount of schema analysis would have found them, because the schema had already collapsed the four meanings into one.
The ordering I would insist on is contexts first in a monolith, extraction later. Modules with boundaries enforced in the build, each context owning its own tables, and six months of living with them. In the worked example two boundaries moved during that period, and extracting immediately would have made both mistakes permanent as distributed transactions.
On the anti-corruption layer, the property that separates a real one from a nominal one is that
it translates semantics rather than field names. Mapping CRED_LIM to creditLimit is
renaming; mapping their -1 sentinel to your None because it means "unlimited" is translation.
And nothing outside the ACL should ever use the foreign vocabulary, which is worth enforcing with an
architecture test rather than a convention.
Where I would push back on the orthodoxy: a ubiquitous language is not company-wide. A single organisational glossary is the God-object failure in documentation form: it forces one definition of "customer" and every team quietly means something else anyway. The language is ubiquitous within a context, and translation between contexts is correct rather than a failure.
Follow-up Q&A
"What is a bounded context?" An explicit boundary within which a model and its terms have one consistent meaning, and outside which the same word may mean something else. That permission is the point: "customer" means a lead with a pipeline stage to Sales, a legal entity with a tax ID to Billing, and whoever is emailing us to Support. Forcing those into one model produces a class with ninety-four columns that nobody can change without consulting four teams.
"How do you find the boundaries?" In the language, not the schema. Interview each team separately and listen for the same word meaning different things, or different words for the same thing, or a model where half the fields are null for half the consumers. A word that needs qualifying, "well, the sales customer", is a boundary announcing itself. In one case a two-day language audit found four meanings of "policy" inside a single ninety-four-column table, and no schema analysis would have found them because the schema had already collapsed them.
"Is a bounded context a microservice?" No. A bounded context is a model boundary; a service is a deployment boundary. They often align and aligning them is a good default, because a service spanning two contexts holds two models and grows a God object. But a modular monolith can hold several contexts perfectly well with boundaries enforced in the build, and that is frequently the right shape.
"So when do you extract?" After living with the boundaries. Contexts as modules first, each owning its own tables, boundaries enforced by the build, and six months of real use. In the case I worked, two of the boundaries moved during that period because the first cut was wrong, and extracting immediately would have turned both mistakes into distributed transactions that would have persisted for years.
"What is an anti-corruption layer and when do you need one?" A translation layer at a boundary that stops another context's model leaking into yours, and you need one whenever you integrate with something you do not control, especially a legacy system. Without it, their nullable everything, their magic status codes and their thirty-year-old enums propagate into your domain and stay there after the system is retired. With it, replacing that system is a change to one class.
"What makes an ACL real rather than nominal?" Two things. It translates semantics, not field
names: mapping CRED_LIM to creditLimit is renaming, and mapping their -1 to your None
because the sentinel means unlimited is translation. Most failed ACLs are renaming layers. And
nothing outside it uses the foreign vocabulary, which is worth enforcing with an architecture test,
because the moment CUST_TYPE_3 appears elsewhere the boundary has leaked.
"What are the context map relationships for?" They describe power and obligation rather than technology. Customer/supplier means the upstream team accepts an obligation to the downstream one, which is a management agreement rather than an interface. Conformist means the downstream has no influence and adopts upstream's model wholesale. Open host means upstream publishes a designed protocol rather than exposing internals, which is right when there are several consumers. And separate ways, meaning no integration, which is under-used: two contexts that would need expensive translation and share little are sometimes better duplicating a little data.
"Is the ubiquitous language company-wide?" No, and that is the most common misreading. A single organisational glossary is the God-object failure in documentation form: it forces one definition of "customer" and every team quietly means something else anyway. The language is ubiquitous within a context, and translation at the boundaries is correct rather than a failure to standardise.
"Isn't DDD mostly aggregates and value objects?" That is the tactical half, and Evans has said the emphasis on it was the main misreading of his book. The tactical patterns improve a codebase and are recoverable if you get them wrong. Strategic design determines service and team boundaries, both of which are expensive to change, so it is where the value is at staff level and it is the half that gets read least.
Common misconceptions
"A bounded context is a microservice." One is a model boundary, the other a deployment boundary. They often align and they are different decisions.
"The ubiquitous language should be company-wide." That reproduces the single-model failure in documentation. It is ubiquitous within a context.
"DDD is aggregates and value objects." That is tactical design, which Evans identified as the main misreading. Strategic design is where the leverage is.
"An ACL is a mapping layer." Renaming fields is not translation. It translates semantics, and it must be the only place that knows the foreign vocabulary.
"Find the contexts, then extract the services." Find the contexts, live with them as modules, then extract. Boundaries drawn in a workshop are frequently wrong and become distributed transactions.
Interview delivery note
Lead with the failure it prevents, using the vocabulary example, because it makes the abstraction concrete in one breath: "'Customer' means a lead with a pipeline stage to Sales, a legal entity with a tax ID to Billing, and whoever's emailing us to Support. A single shared model forces one definition that fits none of them, and you get a ninety-four column class nobody can change without consulting four teams. A bounded context is permission for those to be four different models."
Give the technique, because it is what makes this actionable: "And you find the boundaries in the language rather than the schema. Interview each team separately and listen for the same word meaning different things. In one case a two-day audit found four meanings of 'policy' inside a single ninety-four column table, and schema analysis couldn't have found them because the schema had already collapsed them."
State the boundary-versus-service distinction and the ordering: "A bounded context is a model boundary and a service is a deployment boundary. I'd get the contexts right first as modules in a monolith, with boundaries enforced in the build, and live with them for a couple of quarters before extracting anything. In the case I worked, two of the boundaries moved during that period, and extracting immediately would have made both mistakes permanent as distributed transactions."
The ACL point that shows you have built one: "And an anti-corruption layer has to translate semantics rather than field names. Mapping CRED_LIM to creditLimit is renaming; mapping their minus one to your None because the sentinel means unlimited is translation. Most ACLs that fail are renaming layers."
Close by correcting the common emphasis: "and I'd say the strategic half is where the value is. Evans has said the focus on aggregates and value objects was the main misreading of his book. Those improve a codebase and are recoverable. Context boundaries determine service and team boundaries, and those are expensive to change."
Further reading
- Eric Evans, Domain-Driven Design (2003), especially Part IV on strategic design.
- Vaughn Vernon, Implementing Domain-Driven Design (2013), for the practical treatment.
- Alberto Brandolini's writing on Event Storming and context mapping.
- Skelton and Pais, Team Topologies, for the connection between bounded contexts and team cognitive load.
- DDD tactical design, for aggregates as consistency boundaries.