Divergent Change
One module, many unrelated reasons to change. The exact dual of shotgun surgery: there the knowledge had no home, here one home holds knowledge that does not belong together.
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.
Why is this one file in every pull request, no matter what the pull request is about?
The team wants to change the dunning email copy. It is a marketing edit, and it lands in OrderProcessor — the same file the payments team is currently changing to add a new provider.
Everything about processing an order lives together, which is convenient: one place to look, no wiring, no indirection, and the data is already loaded.
It is convenient to *read* and expensive to *change*, and those pull in opposite directions as the number of reasons grows. Each new reason widens the regression surface of every other one.
- It is convenient to *read* and expensive to *change*, and those pull in opposite directions as the number of reasons grows. Each new reason widens the regression surface of every other one.
- Risk stops being separable. A copy tweak now carries the same deployment risk as a payments change, so either the copy change gets over-reviewed or the payments change gets under-reviewed. Teams reliably choose the second.
- The test suite becomes the union of all concerns, so it is slow, and slow suites stop being run locally, which removes the feedback that would have caught the mistake.
- Two teams editing one file at different rhythms produce conflicts proportional to the product of their rates, not the sum.
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.
- Marketing edits are weekly; payment provider work is quarterly and high risk. They cannot be reviewed with the same care.
- The file has one test suite, so a copy change runs the payment tests and a payment change runs the copy tests.
- The team is small enough that a formal ownership split is not realistic — five people, one repository.
- An order must be charged exactly once, whatever else happens in this module.
- A change to presentation must never be able to change what is charged, and today nothing enforces that.
Who owns what, and where the seams fall
Responsibilities decide boundaries; boundaries decide what an interface has to say.
- Charging owns the money invariant, and nothing else may be able to affect it.
- Notification owns copy, templates and channels — a concern with a completely different rate of change and a completely different failure cost.
- Something small must own the sequence, calling the two in order. That coordinator is allowed to change when the *sequence* changes, and for no other reason.
- Split by rate of change first. Weekly and quarterly do not belong in one deployable unit if you can help it (Finding Seams).
- Split by blast radius second: a concern that can lose money and a concern that can embarrass you should not share a review.
- Do not split by data. Both concerns need the order; that is what parameters are for, and sharing a record is not evidence of a shared responsibility (Shared-State Coupling).
The smell, read from history
Like its dual, divergent change is invisible in a single diff and obvious across a hundred. The tell is that the module appears in pull requests whose titles have nothing in common.
- Different rates — weekly copy edits beside quarterly payment work.
- Different risk — a concern that can lose money beside one that cannot.
- Different reviewers — if the right reviewer depends on which part you touched, that is the split.
- Different failure modes — a broken template and a double charge do not belong to the same on-call rotation (Blast Radius: If This Is Wrong, How Much Does It Affect?).
looks like One file in the diff of most pull requests, whatever they are about. Its test suite covers unrelated subjects. Its imports include a template engine, a payment SDK and a CSV writer. Its change log reads like the product roadmap rather than like a component.
suggests Concerns with different rates of change and different risk profiles share a unit of deployment, review and regression. Every concern is now priced against the union of all of them.
fix Take the reasons to change from six months of history, group them, and extract the group whose rhythm differs most from the rest — keeping a thin, explicit coordinator so the sequence is still readable in one place.
The duality, stated exactly
These two smells are the same variable read in two directions, and seeing that is what stops a team from oscillating between them. Take the relation "requirement R causes a change in module M". Shotgun surgery is one R with many Ms. Divergent change is one M with many Rs.
That is why fixing one carelessly produces the other. Consolidating seven scattered sites into a single module fixes the first and, if you consolidate concerns that were never one concept, creates the second. The target is not "few modules" or "many modules": it is that the mapping between requirements and modules is close to one-to-one for the requirements you actually get.
| Shotgun Surgery | Divergent Change | |
|---|---|---|
| The relation | One requirement → many modules | One module → many requirements |
| What is missing | An owner for a piece of knowledge | A separation between pieces of knowledge |
| Where you see it | In one wide commit | In many narrow commits touching one file |
| What it costs | Discovery, coordination, and the site you missed | Regression surface, slow tests, conflicts, mismatched review |
| The fix | Consolidate the sites that share knowledge | Split the concerns that change at different rates |
| How the fix fails | You consolidate look-alikes and create divergent change | You split too far and create shotgun surgery |
| Genuinely fine when | The wide change is one-off and mechanical | The module only sequences steps and owns none of them |
Splitting by rhythm, not by noun
The failed split is the one that follows the nouns already in the file. The useful split follows the reasons, which usually cut across the nouns and produce module names that did not previously exist in the code.
order/ OrderData.ts // the record OrderService.ts // charge + notify + export OrderHelper.ts // whatever was left // A copy change still touches OrderService. // A payment change still touches OrderService. // Nothing about the cost of change moved.
billing/Charge.ts // changes when billing rules change notify/DunningEmail.ts // changes when copy or channel changes orders/ProcessOrder.ts // changes when the SEQUENCE changes // 12 lines in ProcessOrder: charge, then notify, then record. // The other two never appear in each other's tests.
The first split preserves the property that made the module expensive — every reason to change still lands in one file — and adds two more files to the change. The second removes it: the weekly concern and the quarterly concern now have separate suites, separate reviews and separate deploy risk. The names in the second version could not have been produced by looking at the code, only by looking at why it changed.
How to build it
Most important first.
- List every reason the module has changed in the last six months, from history rather than from opinion. Group them; the groups are your candidate modules (Designing by Responsibility).
- Extract the group with the *most different* rate of change from the rest, not the largest one. Frequency mismatch is what makes the pain.
- Keep the coordinator explicit and thin, so that the sequence of steps is still readable in one place — losing that readability is the real cost of splitting (Transaction Script).
- Let the extracted concern depend on a domain event or a plain value, not on the coordinator, so the dependency runs one way (Dependency Direction).
- Stop when each remaining module has a story: "this changes when billing rules change" is a story; "this changes when anything about orders changes" is not (Single Responsibility, Carefully).
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.
- Before: a copy change costs a full-module test run, a review by people who care about payments, and a deploy that carries payment risk. Its true cost has nothing to do with its size.
- Before, compounding: each new concern added to the module raises the cost of every existing concern's changes, so the module gets more expensive to edit precisely as it becomes more central.
- After: a copy change costs one small module, a fast suite and a low-risk deploy. A payments change costs a careful review of a module that only ever contains payment logic.
- What got more expensive: a change to the *sequence* — charge before notify, or notify then charge — now touches the coordinator and both modules' interfaces, where before it was two adjacent lines.
- You lose the single narrative. Reading "what happens when an order is processed" now means opening three files, and for a new joiner that is genuinely worse.
- You gain a wiring layer and an internal contract, both of which need maintaining and neither of which existed before (Internal Module Contracts).
- Rate-of-change splitting optimises for the recent past. If the rates change, you have optimised for a distribution that no longer holds.
What can go wrong
- Split by noun rather than by reason —
OrderData,OrderService,OrderHelper— and every reason to change still touches two of the three. - The coordinator absorbs the logic that was hard to place and quietly becomes the new divergent module.
- The split is done for a module that genuinely has one reason to change expressed in several code shapes, which adds wiring and buys nothing (Over-Decomposition).
- The mitigation fails: rate of change is measured from history, and history is a poor guide right after a product pivot, when the past six months describe a system that no longer exists.
- The coordinator depends on both concerns; neither concern depends on the coordinator or on each other. That shape is what keeps a copy change out of the payment tests.
- Both depend on the order value, which should be a value object rather than a mutable record they both write to (Value Objects).
- "A module should do one thing." Nobody can define "one thing", which is why the useful form is "one *reason to change*" — a statement about the business, not about the code (Single Responsibility, Critically).
- "So split until each file is small." The dual smell is waiting on the other side: split too far and one requirement starts touching six modules again (Shotgun Surgery).
- "Divergent change and god object are the same smell." Related but not identical. A god object also has a huge API and many dependencies; a small, tidy 200-line module can have divergent change and nothing else wrong with it (God Object).
- "We can fix it with better folder structure." Moving files does not change what a change touches. If the reasons stay in one module, the folder is decoration (Decomposition by Folder).
- divergent-change
- god-object
- shotgun-surgery
Testing it, and how it ages
- After splitting, each module's suite should fail for exactly one kind of reason. If the notification tests break when you change proration, the split is not real (Testing as Design Feedback).
- Keep one integration test over the coordinator asserting the order of effects, because that is the knowledge the split scattered.
- Test the money invariant at the charging boundary, where it can no longer be reached from the presentation path.
- Divergent change accumulates through convenience: the data is already here, the transaction is already open, the reviewer is already familiar. Each addition is locally rational.
- It is often the second half of a lifecycle whose first half was shotgun surgery: a team scattered by consumers, over-corrected into one place, and now has the dual problem.
- The split ages well if the rates stay different and badly if the concerns converge — when the copy really does depend on the payment outcome, the boundary starts leaking and should be revisited (Revisit Triggers).
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.
- GENERALCounting reasons to change works for any unit of packaging — class, module, package, service — and the finding transfers; what differs is how expensive the split is, since splitting a class is an afternoon and splitting a service is a quarter.
- SCALE-SPECIFICAt one team, divergent change costs conflicts and slow tests and little else. At several teams it becomes an ownership problem: two groups with different risk tolerances share one review and one deploy, and the more careful group always loses.
- CONTESTEDThe strongest opposing view is that co-locating everything about a concept is the single biggest readability win available, and that "reasons to change" is unfalsifiable in practice because you can always describe two reasons as one at a higher level of abstraction. That criticism is fair — the count depends on how you phrase the reasons — and the way to keep it honest is to take the reasons from commit history rather than from a whiteboard.
Where the depth lives
This domain teaches the codebase-level structure and hands the rest off.
- — Testing & Reliability Engineering — "each module's suite fails for one kind of reason" is a testability claim, and the diagnostic value of a failing test's specificity is developed there.