Software Design Reference Master Designer · Evolution Trade-Offs

Lesson 0036 · Master Designer · Module 3

Isolation vs Integration

Isolated parts evolve independently; integrated parts stay consistent. The bounded context is the master's unit of this trade: isolation where meanings differ, integration where they can't.

Reading ≈ 9 minPractice ≈ 30 min

Mission tie-in: the second Evolution Trade-Off — lesson 0009's boundaries, now at the scale of whole contexts and their shared meanings.

Knowledge: the context is the unit

Isolation — a context owns its model, its schema, its vocabulary. It evolves at its own speed; other contexts can't break it, and it can't break them. Cost: duplicated concepts across contexts (lesson 0028's duplication — but here it's earned: the "customer" in sales and the "customer" in support are genuinely different knowledge).

Integration — contexts share a model, a schema, an API. Consistency is guaranteed; divergence is impossible by construction. Cost: change amplification — one context's change ripples through all the others (lesson 0006 at system scale).

The principle behind the principle

The two forces are independent evolution — each side can change its model without asking permission — and guaranteed agreement — the two sides cannot drift, because there is only one thing to drift from. The unit of decision is the concept: one word, and the rules that hold for it.

The mechanism is that a model is a compression of the domain for a purpose, and different purposes compress differently. Merge two purposes into one model and every future change must satisfy the union of both constraint sets. Add a third purpose and the union grows again. This is why the one-true-model does not become unchangeable through size: it becomes unchangeable because the set of people who can veto a change keeps growing, and each of them is right about their own half. Isolation buys back changeability by keeping each constraint set small, and pays for it in translation and in facts that exist in more than one place.

Notice what that mechanism does not depend on: how many fields the two sides share. Two contexts can overlap in 90% of their columns and still be two concepts, because the deciding test is whose rules must hold, not whose data looks similar. And the mechanism inverts where the purposes are genuinely one: if both sides enforce the same rules and change at the same time, isolation duplicates knowledge rather than compressing it, and you have bought translation cost with no changeability in return.

The deciding test is meaning, not overlap. When two teams use the word "order" to mean the same thing with the same rules, integration is honest. When their rules differ — sales' "confirmed order" vs warehouse's "picking order" — they are two concepts wearing one name, and forcing a shared model means every change negotiates the difference forever.

# integration where meaning is shared: a contract both sides truly share
class OrderConfirmed(Event):      # both contexts agree on this fact
    ...

# isolation where meaning diverges: each context owns its vocabulary
# sales.Order has "confirmed", warehouse.PickingOrder has "ready to pick"
# they meet only through the event — the anti-corruption boundary

The instruments: bounded contexts (isolation drawn at meaning), an anti-corruption layer (the adapter that translates between two vocabularies — lesson 0008's Adapter at context scale), and shared kernels (the small, stable set of concepts that genuinely must be shared, integrated on purpose).

Worked: one word, three sets of rules

A retailer has one returns table with 34 columns, written by three teams. Support treats a return as a case: notes, reasons and outcomes stay editable, because agents amend them while talking to the customer. Finance treats it as a refund transaction: once the refund posts, the record is immutable, because the tax position depends on it. The warehouse treats it as an expected parcel: it cares about a tracking number and an arrival, and nothing else on the row.

The first thing a lead looks at is not the column list — eleven of the 34 columns are genuinely used by all three, and that overlap is what keeps the shared table looking reasonable. The first thing to look at is the invariants: support requires editability, finance requires immutability. Those two rules cannot both hold for one record, so the shared table has already resolved the conflict, silently, with a locked flag and a convention. Every support feature now needs a finance reviewer, not because finance owns the feature, but because finance owns a rule that lives in the same row.

The cost is visible in lead time, which is the number worth collecting before the meeting. Migrations on the shared table need three sign-offs and take about nine days from proposal to production; migrations on a single-owner table take about one. With fourteen migrations across the three teams last year, that is roughly 112 days of waiting — not of work — spent on a coupling nobody chose.

The alternative the lead rejects first is the compromise: extract a shared returns-core library so everyone uses the same model "properly". That does not remove the negotiation; it relocates it into a release cycle. Three teams now coordinate on a version bump instead of a migration, with the added charm that upgrading is optional, so the drift becomes invisible instead of merely expensive.

The second rejected alternative is maximal isolation: three fully independent models with no shared anything. Something genuinely is shared — the return's identity and the fact that a return was requested. Without a common identifier and a common event, no one can join the three views, and the finance reconciliation that used to be a query becomes a project. So the shared kernel is exactly two things, deliberately: the return id, and ReturnRequested. Everything else — status vocabulary, editability rules, retention — diverges, and each side translates at its own edge.

A year later, the metric that shows it worked is not "number of services". It is the count of nullable columns that exist only for another team's purpose, and how often a code change waits on a stranger's calendar. Support ships copy changes daily; finance's model stops accreting fields it does not understand; and the words status2 never enter the schema.

† The failure pattern is the "one true model": a single schema everyone must fit — integration by decree. It works until the first genuine divergence, then every change becomes a committee meeting (lesson 0033's coordination debt).
Field notes · what it looks like in real code
You see thisWhat it costsThe move
one table read by two teams’ servicesIntegration by schema: every migration is a negotiation.One owner; the other consumes events.
a shared “core” package that only growsThe one-true-model, accreting by default.Split it; keep only what both sides genuinely share.
a field named status2 or type_for_billingOne model bent to hold two meanings.Two models, one translation at the seam.
an import from another context’s internalsThe anti-corruption layer was skipped.Route through the published contract instead.
What people get wrong after reading this
"Isolation means duplicating data, and duplication is bad."
DRY is about knowledge that changes together, not about facts that appear twice. A projection each context owns, fed by an event and derived from one authoritative source, is not duplicated knowledge — it is one truth with several readings, which is what a read model is.
"A bounded context needs its own service and database."
A bounded context is a boundary of meaning. Two packages inside one deployable, with no imports between their internals, is a perfectly good bounded context. Whether to split the deployment is a separate question decided by different forces (lesson 0033) — and conflating the two is how teams end up with distributed systems they did not need.
"If we design the canonical model carefully enough, translation goes away."
Translation is not a symptom of insufficient care; it is the permanent cost of having more than one purpose. The canonical model is not wrong once and then fixed — it is wrong continuously, because each purpose keeps evolving in its own direction.
"A shared kernel is a compromise we should feel bad about."
It is a deliberate, small, jointly owned integration, and it is the right answer for the handful of concepts both sides genuinely share. The failure mode is not having one; it is letting it grow, because everything added to it acquires two owners and a veto.

When this is the wrong move

Drawing contexts too early is the common self-inflicted wound. In a young product the language is still moving; boundaries drawn now cut in the wrong place, and the anti-corruption layer becomes a museum of a model you abandoned six weeks later. One team, one product, unstable vocabulary: keep one model and refactor freely until a second consumer or a second set of rules shows up.

Analytics is the honest counter-case for integration. Cross-context questions — returns per region, refund value by cohort — need conformed dimensions and a single shape, and answering them from four isolated models is exactly the pain that data warehouses exist to remove. Isolate the transactional models; integrate deliberately downstream, in a place where a shared schema costs no one their autonomy.

In the lab: the companion Phase 3 exercise gives you a returns module where one class serves the customer flow, the finance record and the warehouse's expectations at once. You list the rules each purpose enforces, split what the rules force apart, and keep the smallest honest shared kernel between them.

Skill: same meaning or same name?

Two contexts share 90% of an entity's fields. That fact:

Two contexts meet through events and a translating adapter. That adapter is:

A concept both contexts share with identical rules deserves:

Practice on your own code

Find two modules (or teams' code) that share a name for a concept. Write the rules each side holds for it. If the rules match — deliberate shared kernel or honest merge. If they differ — draw the bounded context line and the translating adapter.

Reveal: a meaning audit

"Invoice" in billing and "invoice" in reporting: billing holds the legal lifecycle; reporting holds the display data. The shared table forced every billing change through reporting's migrations. Split: billing owns the invoice lifecycle; reporting consumes InvoiceIssued events and owns its own projection. One table became two contexts, and the migration coupling died with the shared schema.

Teach it back
  1. In two sentences, explain to a junior why two teams sharing most of a table's columns can still need two models.
  2. Find one concept in your own system that two purposes share. Write the rule each side enforces, and measure the cost: how many days does a change to that table or class wait for someone else's approval?
  3. Predict the one-year state if it stays merged: how many nullable columns will exist for another team's benefit, and which change will finally be declared "not worth doing"?

Your win

You can audit any shared concept for meaning, draw bounded contexts at the divergence, and integrate deliberately where meaning is truly one.

Read and watch deeper

Bring your shared-schema pain points to your agent-teacher and draw the context lines together.