Finding Seams
Three probes locate where a boundary should go: follow the change, follow the invariant, follow the rate of change. All three are questions about evidence, not taste.
The requirement, the obvious build, and why it breaks
Every lesson starts where the work starts: someone asked for something, and the first implementation that comes to mind survives until the requirement changes.
I accept that boundaries should contain change. How do I find out where the boundary actually belongs, rather than guessing?
A team agrees the billing module needs splitting. Four people propose four different splits, all defensible, and the discussion has run for two weeks with no decision.
Draw the seam where the nouns are. There is an Invoice, a Subscription and a Payment, so there should be three modules, one per noun. It is quick, everyone can agree on the nouns, and it produces a diagram that looks like the domain.
Nouns are how the domain is *described*, not how it changes. Proration touches all three nouns and belongs to none of them, so it lands wherever the person implementing it happened to be (Domain Modeling).
- Nouns are how the domain is *described*, not how it changes. Proration touches all three nouns and belongs to none of them, so it lands wherever the person implementing it happened to be (Domain Modeling).
- Noun-shaped modules end up mutually dependent almost immediately — invoices need subscriptions, subscriptions need payments, payments need invoices — which is a cycle drawn on day one (Dependency Cycles).
- The invariants get split. "Total equals sum of lines" spans invoice and line item; if those become separate modules with separate stores, the invariant is now a convention between them (Invariant Leaks).
- It is unfalsifiable in the same way "one thing" is: everyone agrees there is an
Invoice, so nobody has to produce evidence, and the two-week argument continues at a different level of detail.
What limits the solution, and what must never stop being true
This domain leads with these two. A design that ignores its constraints is not a design, and an invariant nobody named is one nothing is protecting.
- The code has three years of history, so evidence about what actually changed exists and nobody has looked at it.
- Whatever is chosen has to be implementable in slices; a boundary that requires a two-month branch will not be built.
- One of the four proposals is from the module's original author, which makes the conversation political as well as technical (Tone, Disagreement and Receiving Review).
- Invoice totals must equal the sum of their line items, at every point in the lifecycle (Invariants).
- A subscription's billing period boundary is authoritative: nothing outside the module may compute it independently.
- Whatever seam is drawn, each invariant must end up wholly inside one unit rather than spanning two (Consistency Boundaries).
Who owns what, and where the seams fall
Responsibilities decide boundaries; boundaries decide what an interface has to say.
- Whoever proposes a seam owns producing the evidence for it: which changes it would have contained, taken from real history rather than imagination.
- The module boundary owns exactly one thing once drawn — either a lifecycle, an invariant, or a rate of change. A seam justified by all three at once is usually justified by none (Single Responsibility, Carefully).
- The team owns writing down which probe decided it, so the next argument starts from evidence rather than from scratch (Decision Records).
- Follow the change: replay the last twenty tickets and mark which files each touched. A candidate seam is good if most tickets fall inside one side of it (Change Amplification).
- Follow the invariant: a rule that must hold atomically defines a boundary, because splitting it turns a guarantee into a coordination problem (Where Invariants Live).
- Follow the rate of change: code that changes weekly and code that has not changed in two years do not belong in the same unit, whatever the nouns say (Stability and Dependency Direction).
- The three probes usually disagree slightly, and where they do the invariant probe wins — a boundary that splits an invariant is wrong regardless of how well it contains changes.
Three probes, run in order
Each probe answers a different question and each can be run in under an hour. Their value is that they turn a taste argument into a disagreement about evidence, which is the kind of disagreement that can actually end.
Run them in this order deliberately. The change probe is the cheapest and most often decisive; the invariant probe has veto power; the churn probe is the tie-breaker and the one most easily misread on its own.
- 1Follow the change
Take the last twenty tickets. For each, list the files it touched. Files that appear together repeatedly belong together; a candidate seam is good if most tickets fall entirely on one side.
fails by Using commits instead of tickets, so refactors and formatting runs dominate the co-change signal and everything looks coupled to everything.
- 2Follow the invariant
List the rules that must hold atomically. For each, find the smallest set of data it constrains. A seam may not cut through one of those sets.
fails by Treating a rule that is merely usually true as an invariant, which produces a boundary far larger than it needs to be (Consistency Boundaries).
- 3Follow the rate of change
Count commits per file over a long enough window to span several projects. Separate the weekly-churn cluster from the code that has not moved in two years.
fails by Measuring over one quarter dominated by a single feature, so a temporarily hot area looks permanently volatile.
- 4Cross-check
Overlay the three. Agreement means the seam is obvious; disagreement means the invariant probe wins, then co-change, then churn.
fails by Averaging them into a score, which invents precision the inputs do not have (The Trade-off Matrix).
- 5Draw one seam
Implement the single highest-value boundary, with an enforced import direction, and ship it.
fails by Presenting all four candidate boundaries as a proposal, which turns a pull request into a project that never starts (Incremental Migration).
- 6Record the probe that decided it
One paragraph: which evidence, what it showed, what would move the seam.
fails by Recording the decision without the evidence, so the next disagreement restarts from intuition (Architecture Decision Records).
If all three probes come back flat — everything co-changes, no invariant is local, churn is uniform — that is a legitimate result meaning there is no seam here yet. Drawing one anyway is how a codebase acquires boundaries nobody can defend.
Follow the invariant: a lifecycle is already a boundary
The invariant probe has veto power because splitting an invariant converts a guarantee into coordination. A lifecycle is the clearest case: the set of legal transitions is a rule about the whole object, so whatever code enforces it must see the whole object.
Written out as a machine, the seam draws itself. Everything that can move a subscription between states belongs inside the boundary; everything that merely reacts to a state change belongs outside it and learns by event (State Ownership).
| From | On | To | Guard | Effect |
|---|---|---|---|---|
| trialing | first successful charge | active | payment method on file | set period start |
| trialing | cancel requested | cancelled | — | end immediately; no invoice |
| active | renewal charge fails | past_due | — | start dunning clock |
| past_due | retry succeeds | active | within dunning window | — |
| past_due | dunning window elapses | expired | — | revoke access |
| active | cancel requested | cancelling | — | schedule end at period boundary |
| cancelling | cancellation reversed | active | before period end | — |
| cancelling | period end reached | cancelled | — | — |
- active → cancelled — Cancelling mid-period would stop billing before the paid period ends, breaking the invariant that a customer keeps what they paid for. Every "cancel now" bug in a billing system is this transition existing by accident.
- cancelled → active — Reactivation is a new subscription with a new period, not a resurrection. Allowing it means the period boundary and the invoice history no longer agree, and refunds become unreconstructable.
- expired → active — Same reason, plus a collections consequence: an expired subscription has an unpaid invoice, and reviving it silently would leave that debt attached to a period the customer did not use.
- trialing → past_due — A trial has no charge to fail. If this transition is reachable, something is charging during a trial, which is the failure customers escalate fastest.
The forbidden list is the seam. Every one of those four is enforceable only by code that sees the current state and the transition together — so the boundary goes around the state and the transition function, and no caller anywhere else may write the status field (Invalid Transitions).
Follow the rate of change, and read it carefully
The churn probe is the easiest to run and the easiest to misuse. Its finding is not "the top file is bad" — it is that files with very different change rates should not share a unit, because the stable code is being redeployed and re-tested for reasons that have nothing to do with it.
Read the output as clusters rather than as a ranking. Two files that both changed forty times are interesting; one file that changed forty times because it is the route table is noise.
1# commits per file over a window long enough to span several projects2git log --since='18 months ago' --name-only --pretty=format: -- billing/ \3 | grep -v '^$' | sort | uniq -c | sort -rn | head -204 5# co-change: which files appear in the same commit as pricing.ts6git log --since='18 months ago' --format='%H' -- billing/pricing.ts \7 | xargs -I{} git show --name-only --pretty=format: {} \8 | grep -v '^$' | sort | uniq -c | sort -rn | head -209 10# exclude the noise before believing any of it:11# formatting sweeps, file moves, dependency bumps, generated codeThe second command is the one that finds seams. If pricing.ts co-changes with invoice-render.ts in nineteen of twenty commits, those two are one unit regardless of which folders they live in — and if it co-changes with everything about equally, there is no seam here to find yet.
How to build it
Most important first.
- Run the change probe first, on real history.
git logover the last three months, grouped by ticket, gives a file co-change matrix that no amount of discussion produces (Shotgun Surgery). - List the invariants and mark, for each, the smallest set of data it constrains. Those sets are candidate boundaries and they are not negotiable in the way the others are.
- Compute churn per file. The high-churn cluster wants its own module with its own tests; the stable cluster wants to be left alone, and mixing them means the stable code is redeployed weekly for no reason.
- Cross-check the three. Where they agree, the seam is obvious and the two-week argument ends in an afternoon. Where they disagree, prefer the invariant, then the change probe, then churn.
- Draw exactly one seam and ship it. A proposal with four boundaries is a project; one boundary is a pull request (Incremental Migration).
- Write down which probe decided it and what would make you move it, so the next person inherits the reasoning rather than the outcome (Revisit Triggers).
What the next change costs
The field this whole domain exists for. A structure is only better if it makes the change after this one cheaper — and it is worth saying which changes it does not help.
- Next change, seam drawn where the evidence pointed: it lands inside one module, and the test suite that runs is that module's. The measurable improvement is not elegance — it is that the change stops requiring a full regression.
- Next change, seam drawn on nouns: it crosses two modules, so it needs a coordinated deploy or a compatibility shim, and the invariant that spans the seam needs a new check that did not exist when it was one unit.
- The cost of running the probes is a couple of hours, once, against a two-week argument that has already happened three times. That ratio is the actual argument for this lesson.
- The probes look backwards, and a boundary is a bet on the future. History is the best evidence available and it is still not evidence about what is coming, particularly right after a strategy change.
- They favour boundaries around what has changed recently, which can entrench a hot spot instead of questioning why it is hot.
- Running them takes access to history, tooling and a couple of hours that feel like not-work, and in most teams that time is easier to spend arguing.
What can go wrong
- The probes are run and the result is ignored because it contradicts a diagram someone has already presented (Decision Records).
- The change probe finds that everything co-changes with everything, which is a real finding — it means there is no seam here yet and the honest answer is not to draw one (Premature Abstraction).
- The seam is drawn correctly and enforced nowhere, so within two sprints there are imports across it in both directions.
- The mitigation fails when churn is measured over a period dominated by one large feature, making a temporarily hot area look permanently volatile. Measure over long enough to see more than one project.
- Every seam creates a dependency across it, so the probe also has to ask which way it will point: the stable side should be depended upon, not depend (Stable Dependencies).
- The change probe depends on the ticket history being honest. Squashed commits with messages like "fixes" make it useless, which is a small argument for commit hygiene with a real payoff (Stable Identifiers).
- The churn probe depends on the code not having been reformatted or moved recently, since both produce churn with no meaning.
- "So measure churn and split the top file." Churn without the other two probes finds the file everyone edits, which is often a config or a router — real, and not a design finding (Correlation Is Not the Root Cause).
- "The probes give an objective answer." They give evidence. Three probes disagreeing is normal, and the judgement about which one to weight is still judgement (The Trade-off Matrix).
- "If no seam appears, the design is fine." If everything co-changes, there is no boundary *yet* — that is a finding about a young or genuinely entangled codebase, and the response is to wait for evidence rather than to invent a split (The Rule of Three).
- "A seam is a service boundary." A seam is a place where a change stops travelling. Whether it should also be a deployment boundary is a separate question with a much higher bar (Design, Architecture and System Design).
- shotgun-surgery
- divergent-change
Testing it, and how it ages
- After the seam is drawn, the module on each side should be testable without the other. If it is not, the seam is a folder (Testing as Design Feedback).
- Add a test for the invariant at the unit that now owns it, before moving anything, so you can prove the move preserved it (Characterization Tests).
- Add an architecture test asserting the import direction across the seam, since an undirected boundary decays immediately (Dependency Direction).
- Seams move. A boundary that contained changes for two years stops containing them when the product changes shape, and moving it then is a normal cost rather than an admission of error (Stable Boundaries).
- The change probe is worth rerunning annually. It is cheap, and the answer drifting is the earliest available signal that a boundary is going stale.
- Codebases with no seams do not stay that way for free: as they grow, the change probe result gets worse monotonically, which is what "it got harder to work in" means concretely (What Technical Debt Actually Is).
Where this applies
This domain's advice is contested more than most. These labels say what each claim is specific to — and where CONTESTED appears, the note gives the strongest form of the opposing view rather than a caricature.
- GENERALAll three probes are about where change lands, which is a property of the requirements rather than of the language, so they transfer unchanged to a Go service, a Rails app and a C++ codebase; only the tooling for extracting the history differs.
- SIMULATEDThe co-change and churn figures used in this domain's examples and in the Change Impact Explorer come from Engineer Atlas models over toy repositories, not from measuring real systems. The shape transfers — "these two files always change together" — and any specific number does not.
- LIFETIME-SPECIFICThe probes need history to read. In a codebase under a few months old there is nothing to follow, and the honest answer is to defer the seam rather than substitute intuition for the evidence you do not have yet — which is the opposite of what a greenfield design session usually produces.
Where the depth lives
This domain teaches the codebase-level structure and hands the rest off.
- — System Design — the same probes decide service boundaries, where the invariant probe is much stronger: an invariant split across a network becomes a distributed transaction, which is a category of cost the in-process version does not have.
- — Testing & Reliability Engineering — a seam is credible only if each side can be exercised alone, so the first test written after drawing one is really a check on the boundary rather than on the code.