Lesson 0053 · System Architect · Module 1
Cut Boundaries by Responsibility and Change
Before modules, databases, or services exist, the system has boundaries to find: who owns each decision, what changes together, and what must stay hidden. Responsibility answers it, not naming conventions.
Mission tie-in: every later boundary decision — modules, data ownership, deployables — inherits this map. Finding boundaries from responsibility and change keeps them stable when the technology moves.
Knowledge: responsibility first, structure second
Lesson 0052 produced the concepts and vocabulary. Now assign each concept a responsibility: the knowledge it holds and the decisions it owns. Responsibility-driven design asks three questions of every candidate component:
- Who knows? Which part holds the facts another part needs?
- Who does? Which part may change a fact, and under what rule?
- Who decides? Which part has final say when facts conflict?
For the returns system, one defensible first assignment is:
| Responsibility | Owner | Why there |
|---|---|---|
| Decide eligibility, with reasons | Eligibility | It holds the policy knowledge and the decision rights. |
| Show the customer progress | Return journey | It knows the customer-visible steps and wording. |
| Establish settled money | Payments (external) | Financial truth belongs to the money system, not to us. |
| Produce a shipping label | Carrier (external) | A solved problem we consume, not own. |
Responsibilities cluster into boundaries along two tests, both about change:
- The change test: if the policy rules change weekly, and carrier integration changes on the vendor's cadence, those two responsibilities will diverge. Each earns its own seam.
- The ownership test: a boundary becomes operationally credible when its decision rights and authoritative writers are explicit, even if one small team currently owns several boundaries.
A boundary is a bet about which changes arrive together. Its value is the change it localizes; its price is the coordination it adds to every change that has to cross it. So the boundary you want is the one that minimizes the number of future changes touching both sides — which makes this an empirical question, not a taxonomic one. Nouns are the wrong input because nouns are the stable part of a domain; decisions are what change, and that is why responsibility (who knows, who does, who decides) predicts the seam better than any diagram of entities.
The boundary conditions follow directly. On a greenfield system you have no change history, so you are substituting the experts' estimate of cadence for evidence — expect to be wrong about at least one seam and pick the cheapest kind of boundary to move (a module, not a service). In a system small enough that nearly every change touches everything, boundaries cost more than they save; one cohesive module is the honest answer. And Conway's law outranks your table: if the organization will hand you four teams whatever you draw, a boundary map that ignores those four teams will be eroded by the people who need to ship without asking each other.
The cohesion side comes from lesson 0001: what belongs together is what changes together and is used together. The coupling side sets the rule: dependencies point from the variable world toward the stable one — the policy engine depends on nothing; the carrier adapter depends on the engine's vocabulary, never the reverse.
Two warnings keep this honest. First, a shared database does not establish a boundary. Multiple components may share a store while one authoritative writer owns each fact, but unrestricted writes erase that ownership. Name the writer of every table. Second, don't cut boundaries you don't need yet: a six-person team does not need eight deployables or elaborate contracts. Preserve meaningful model seams, then add structural cost only where change or ownership demands it.
# A boundary as data: the responsibility, its owner, and what changes it.
# This is the spec you will enforce in module organization (lesson 0022).
from dataclasses import dataclass
@dataclass(frozen=True)
class Responsibility:
what: str
owner: str
changes_with: str
BOUNDARIES = [
Responsibility("Eligibility decisions with reasons",
"eligibility", "policy rules"),
Responsibility("Customer-visible progress",
"return_journey", "communication wording"),
Responsibility("Settled money truth",
"payments_external", "payment rail"),
Responsibility("Shipping labels",
"carrier_external", "carrier API"),
]
Worked: letting the change log draw the line
The returns work is not greenfield — returns are handled today inside the order platform's support tooling. So the first thing the architect opened was not a whiteboard but git log, restricted to the returns-handling code, for the previous twelve months: 63 commits. The question was not “what are the entities?” but “what changed, and what changed with it?”
| Area | Commits | Also touched policy rules | Reading |
|---|---|---|---|
| Policy rules | 38 | — | The fast-moving part; weekly cadence. |
| Carrier integration | 11 | 2 | Vendor cadence; almost never co-changes. |
| Customer notifications | 9 | 3 | Wording changes on its own beat. |
| Return journey / status | 17 | 17 | Never changes without the policy changing. |
| Payment call | 5 | 0 | Owned elsewhere; we only call it. |
# Co-change, computed rather than argued about.
COMMITS = [ # one entry per commit: the areas it touched
{"policy", "journey"}, {"policy"}, {"carrier"}, {"policy", "journey"},
{"notifications"}, {"policy", "carrier"}, {"payments"}, {"policy"},
# ... 55 more, extracted from twelve months of history
]
def co_change(commits: list[set[str]], a: str, b: str) -> int:
"""How often a change to a forced a change to b in the same commit."""
return sum(1 for touched in commits if a in touched and b in touched)
def seam_is_cheap(commits: list[set[str]], a: str, b: str,
tolerance: float = 0.1) -> bool:
"""A seam is cheap when few changes have to cross it."""
involving_a = sum(1 for touched in commits if a in touched)
return co_change(commits, a, b) <= tolerance * involving_a
Two of the rows decide the design. Carrier integration co-changed with policy twice across 38 policy changes and 11 carrier changes — under 5%. That seam is nearly free: put a contract between them and almost nothing has to cross it. The return journey co-changed with policy 17 times out of 17 — it never moved on its own. Splitting those two would have meant that every one of the 38 policy changes now needs a coordinated change on both sides of a boundary, which is the exact cost a boundary is supposed to remove.
The alternative the architect rejected was the one the team wanted: extract eligibility as its own service now, “so it is clean from day one”. The change log says the seam between eligibility and the journey is the least justified one in the system, and lesson 0059 prices what a process boundary costs. The rejection was recorded with its reversal condition: if a second team takes ownership of policy, or if the journey stops co-changing with it for two quarters, the seam has become real and the extraction becomes a three-week job instead of a rewrite — precisely because the module boundary was kept enforced in the meantime.
The consequence over time is the part worth internalizing. The seams you place well are invisible: nobody notices the coordination that did not happen. The seams you place badly announce themselves as a standing tax — every release note that says “deploy A before B”, every pull request that spans two repositories for a one-line rule change.
| You see | What it hides | The move |
|---|---|---|
| One giant "everything" module | Ownership is unassigned. | Ask who knows, who does, who decides. |
| Two things change on different beats | A candidate seam. | Test whether separation reduces change cost. |
| A shared table with no writer | Ownership is fictional. | Name the writer or split it. |
| A seam nobody needs | Fashion over evidence. | Merge; cut it when change demands. |
Teams that adopt bounded context vocabulary will recognize this as boundary-finding too — they call the seams contexts and label relationships between them on a context map. That vocabulary is a useful refinement when two teams must coordinate; it is not required to find good boundaries. The responsibility questions above work with or without it.
- “Boundaries follow the entities.”
- Entities are the stable nouns; boundaries are about the volatile decisions. Two components can both read
Returnand still belong apart, because one decides eligibility weekly and the other formats a carrier payload on the vendor's schedule. - “A boundary is only real if it is a separate service or repository.”
- A boundary is real when crossing it is checked. An enforced import rule in one codebase is a stronger boundary than two services that share a database, and it costs nothing at runtime. Choose the weakest enforcement that actually fails a build.
- “Sharing a database means sharing a boundary.”
- Ownership is about the writer, not the connection string. A shared store can preserve boundaries when each table has one authoritative writer — but only for as long as that rule is enforced by something other than good intentions, because the first cross-module
UPDATEunder deadline pressure is silent. - “Get the boundaries right up front.”
- You will get one or two wrong. The useful goal is not correctness but cheap correction: keep the mistakes inside a codebase where moving a seam is a refactor, not a data migration and a contract negotiation.
When this is the wrong move. Do not cut boundaries into a system whose change history says everything moves together — that is a cohesive core, and splitting it converts every change into a coordination problem. Do not cut them in a codebase small enough to hold in your head, or in one owned by a single person for the foreseeable future; the ceremony has no beneficiary. And do not cut a boundary you cannot enforce: an unenforced seam decays into a naming convention within two quarters, and then the map lies, which is worse than having no map. Merge instead, and wait for change to give you evidence.
Skill: find the seam from the change
A boundary becomes operationally credible when…
Two responsibilities belong together when…
A shared database between two parts means…
Practice: assign responsibility before structure
Take the model from lesson 0052, using either your domain or the supplied returns case. For every concept, answer who knows, who does, and who decides. Cluster the answers by the change and ownership tests, then record candidate boundaries as a table — name, responsibilities, decision rights, authoritative facts, and reasons to separate. Identify one candidate you would keep together for now and state what future evidence would justify splitting it.
In the lab and the capstone: the companion lab's phase-5 checkpoint starts from a returns-resolution service with no enforced seams at all. Run the same analysis there — read what the code's own history and its callers say changes together, then propose boundaries and back each one with a rule something can check. The worked capstone shows where this reasoning ends up in its workflow and drivers artifacts.
Reveal: the two-writer test
Pick any fact both of your parts could plausibly write — a status, a count, a decision. If both can write it, you have not assigned responsibility. Name one writer, make the other a consumer of a contract, and the boundary appears by itself.
- Explain to a senior engineer, in two sentences, why co-change evidence beats a diagram of entities when placing a boundary.
- Take one system you own this week: name the two parts that most often appear in the same pull request, and say whether your current boundary agrees with that fact.
- Predict the damage: if a team splits two responsibilities that always change together, name what shows up in their release process a year later.
Your win
You can find system boundaries from responsibility and change — who knows, who does, who decides — and catch unowned state before it becomes a shared table or a service with no owner.
Read and watch deeper
- Applying UML and Patterns, Craig Larman — ch. 17–18: GRASP — assigning responsibilities by knowing, doing, and deciding.
- Object Design: Roles, Responsibilities, and Collaborations, Wirfs-Brock & McKean — responsibility-driven design, collaborations, and control style.
- A Philosophy of Software Design, Ousterhout — ch. 7–9: layers, pulling complexity downward, better together or apart.
- Team Topologies, Matthew Skelton & Manuel Pais — ch. 2–4: Conway's law, cognitive load, and why an ownership boundary the organization cannot staff will not survive contact with delivery.
- Related lessons: 0036 (the trade-off), 0044 (the diagram side); glossary consistency boundary.
Give your agent-teacher your responsibility table and ask it to argue that two rows belong to the same component — then rebut with the change and ownership evidence.