Skip to the content
Software Made Clear Diagrams that show the mechanism About

Bounded contexts, concretely

ANSWER

Look for the word that means two different things in two parts of the system — customer, order, account — and put the boundary there. Each side keeps its own model of that word, and only an identifier crosses between them.

IN PLAIN TERMS

Two teams in one company each keep their own list of customers, like two address books in the same house. Sales writes down who might buy; the dispatch desk writes down where to deliver. One shared list would make both worse, so all that crosses between them is the customer number.

You have read the definition — a boundary within which a model means one thing — and it did not tell you where to draw one in the code in front of you. Here is a test you can run this afternoon instead. Take the nouns that appear everywhere in your system, and for each one ask what it has in each part of the code. Where the answer changes, you have found a boundary. It was already there; the only question is whether your code admits it.

Find the boundary by finding the word#

Run the test on customer, which every system has and which two parts of a system rarely define the same way.

A customer in sales has a pipeline stage, a lead source, and a rep who owns the relationship. It has no address anyone would post a letter to, because at that stage nobody has one. A customer in dispatch has a delivery name that must match the signed receipt, a postal address, access notes and a service level; it has no pipeline stage, because by the time you are invoicing someone the pipeline is over. A customer in support has a contract tier, a ticket history and an escalation path, and cares about none of the above.

Three sets of attributes, three lifecycles, three teams each of whom will tell you theirs is the real customer. All three are right. The word is doing three jobs, and the jobs are genuinely different.

two contexts, two models
# sales/models.py
@dataclass
class Customer:
    id: CustomerId
    company_name: str
    stage: PipelineStage       # prospect, qualified, won, lost
    source: LeadSource
    owner: SalesRepId

# dispatch/models.py
@dataclass
class Customer:
    id: CustomerId
    delivery_name: str         # must match the signed receipt
    address: PostalAddress
    access_notes: str | None
    service_level: ServiceLevel

The argument is how short each one is. Neither class is missing anything: each holds exactly what its own side of the system needs to do its work, and nothing it would have to leave empty.

You can see the symptom when a codebase refuses this. One class tries to be all three, and it grows fields that most callers ignore — a table with nullable columns nobody can explain, a constructor with eight optional arguments, a validation rule that is only correct when called from one place. The comment # only set for dispatch customers is the boundary, written down as an apology instead of as a decision.

Two things are worth being precise about. The first is that the boundary goes where the meaning changes, not where the org chart happens to split. Those often coincide, and when they do not, the meaning wins — a team that owns two meanings owns two contexts, and pretending otherwise just moves the confusion inside the team. The second is what the boundary buys you, which is smaller than people expect and more useful: inside it, one word means one thing, so a rule about customers can be written without a qualifier. That is the whole benefit. Everything else follows from being able to say something true in one sentence.

This is what the pattern language calls a ubiquitous language, and the part most often misquoted is its scope. The shared vocabulary lives inside one boundary, not across the system. There is no goal of getting the whole company to agree on what a customer is. The goal is that within each context, everyone — including the code — uses the same word the same way.

What crosses, and what stays#

The model does not cross. An identifier crosses, plus whatever minimum the other side needs, restated in the other side’s own terms. Dispatch does not receive a sales customer; it receives a customer id and the few facts it needs to plan a delivery, expressed the way dispatch already talks.

Somewhere a piece of code turns one into the other, and the only real decision is whether that code exists on purpose in one place or by accident in fifteen. Written on purpose it is an anti-corruption layer: a translation seam at the edge of your context whose job is to stop the other side’s model leaking into yours. Written by accident it is a sales stage field appearing in a delivery note, and then in a report, and then in a query that nobody can change because three teams read it.

Between any two contexts there is a relationship, whether or not anyone named it. Naming it is useful because each one commits you to something different.

RelationshipWhat it means in practiceWhat it costs
Shared kernelBoth sides share a deliberately small piece of model and own it jointly.Neither side can change the shared part alone. Works when the two teams talk daily; fails quietly when they stop.
Customer–supplierOne context depends on another, and can put its needs on the other’s backlog.A real commitment by the upstream team. Without it the label is aspirational and you are actually a conformist.
ConformistYou take the upstream model exactly as it is and do not translate.Cheapest to build, and upstream’s model is now yours — including the parts that make no sense on your side.
Anti-corruption layerYou translate at your edge and keep your own model clean.Code to write and keep working. It is the price of not letting a model you do not control set your vocabulary.

These are relationships between two contexts, not qualities of one. The same context is usually a conformist to one neighbour and runs a translation layer against another, and that is normal rather than inconsistent.

The next published stop on this path takes one such boundary — a write API and the systems that retry against it — and works through what the contract across it has to promise: idempotency keys and safe retries. The architecture path holds the rest in the order that makes each step explain the next.

The shared core library#

It arrives with impeccable reasoning: two contexts both have a Customer, somebody notices the duplication, and the common parts are extracted into a package both sides depend on. Nobody objects, because nobody argues in favour of duplication.

Then the two models start to move apart, because they were never the same model.

01

A one-line change needs three releases

Dispatch needs a field. The field goes in the shared package, the package is released, and every other context upgrades whether it wanted the field or not. A local change became a coordinated one.

02

The shared class fills with optional fields

Each one means not applicable here, and which here it refers to is not written anywhere. Every consumer carries a null check for a case that cannot occur on its side.

03

Nobody can say no

The package belongs to everyone, which means it belongs to nobody. A change that is wrong for two contexts and right for one lands anyway, because there is no owner positioned to refuse it.

The uncomfortable conclusion, and the one that gets argued about: duplicating the class was the cheaper option. The two things were never the same thing, so what looked like duplication was two similar-looking definitions of different concepts — and merging those does not remove work, it converts private work into negotiated work. The fields drift apart on their own, and the shared package spends its life resisting.

That does not retire the shared kernel: it is a real pattern, and sometimes exactly right. What makes it right is not the absence of duplication. It is joint ownership by two teams who genuinely collaborate, over a surface small enough that both can hold it in their heads — a set of identifiers and a couple of value types, not an entity with a lifecycle. A shared kernel that has grown a lifecycle has stopped being a kernel and become a third context that nobody is responsible for.

IF YOU REMEMBER ONE THING

A bounded context is not something you introduce. It is something you find, by noticing that a word already means two things — and then either drawing the line or paying for it in nullable columns.

A boundary drawn on a whiteboard costs nothing. It starts costing something at the point where a single unit of work has to span two of them, which is where the transaction boundary stops being an implementation detail.

Questions people also ask

5 QUESTIONS
How big should a bounded context be?

Big enough that every word inside it has one meaning, and no bigger. That is a property you can check rather than a size you can guess: walk the nouns, and if one of them needs a qualifier to be understood — a sales customer, a dispatch customer — the context has grown past a boundary that is already there.

Is a bounded context the same as a microservice?

No, though they are often confused because a boundary is a sensible place to cut a service. A context is a boundary in the model; a service is a boundary in deployment. One context can be several services, and a small system can hold three contexts in one deployable with no network between them at all.

Can two contexts share a database?

They can share an instance without trouble. Sharing tables is the thing that undoes the boundary, because the schema then encodes one model that both sides must agree on, and a change for one becomes a migration for both. Separate schemas with no cross-schema reads is the usual compromise.

What if two teams disagree about what a word means?

That disagreement is the finding, not the problem. Two meanings mean two contexts, and the work is to name both explicitly rather than negotiate a single definition that fits neither. Someone does have to arbitrate what crosses between them, and that is a decision worth writing down.

Do I need domain-driven design to use bounded contexts?

No. The test in this article — find the word that means two things, put the boundary there — needs no vocabulary beyond itself. The wider pattern language is useful when you have several boundaries and need to describe the relationships between them, which is a later problem than finding the first one.