DecompositionGENERALSIMULATEDLIFETIME-SPECIFIC

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.

The question

I accept that boundaries should contain change. How do I find out where the boundary actually belongs, rather than guessing?

The requirement

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.

The obvious build

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.

Why it breaks

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).

How it breaks as requirements change
  • 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.
RequirementConstraintsInvariantsResponsibilitiesBoundariesInterfacesStateDependenciesFailureImplementationTestsFeedbackEvolution

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.

Constraints
  • 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).
Invariants
  • 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.

Responsibilities
  • 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).
Boundaries
  • 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.

Locating a boundary from evidence
  1. 1
    Follow 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.

  2. 2
    Follow 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).

  3. 3
    Follow 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.

  4. 4
    Cross-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).

  5. 5
    Draw 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).

  6. 6
    Record 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).

Subscription lifecycle — the boundary is whatever enforces this
trialingactivepast_duecancellingcancelled ·expired ·
FromOnToGuardEffect
trialingfirst successful chargeactivepayment method on fileset period start
trialingcancel requestedcancelledend immediately; no invoice
activerenewal charge failspast_duestart dunning clock
past_dueretry succeedsactivewithin dunning window
past_duedunning window elapsesexpiredrevoke access
activecancel requestedcancellingschedule end at period boundary
cancellingcancellation reversedactivebefore period end
cancellingperiod end reachedcancelled
must be impossible
  • active → cancelledCancelling 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 → activeReactivation 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 → activeSame 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_dueA 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

SIMULATEDAny co-change or churn number quoted in this domain's lessons and labs is generated by an Engineer Atlas model over a toy repository, not measured from production code. Run the commands above on your own repository to get numbers that mean something; the transferable part is the method and the exclusion list, not any figure.

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.

The whole churn probe
1# commits per file over a window long enough to span several projects
2git log --since='18 months ago' --name-only --pretty=format: -- billing/ \
3 | grep -v '^$' | sort | uniq -c | sort -rn | head -20
4
5# co-change: which files appear in the same commit as pricing.ts
6git 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 -20
9
10# exclude the noise before believing any of it:
11# formatting sweeps, file moves, dependency bumps, generated code

The 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 log over 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.

Cost of the next change
  • 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.
What the recommended approach costs
  • 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

Failure modes
  • 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.
Dependencies, and their direction
  • 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.
Misreads
  • "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).
Smells this explains
  • shotgun-surgery
  • divergent-change

Testing it, and how it ages

What to test, and at which boundary
  • 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).
How this design ages
  • 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.

Architecturemodular-monolith
Domains that do not exist yet
  • 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.