Divergent change & shotgun surgery
Divergent change and shotgun surgery are opposite SRP smells you read from git history, not from arguing about responsibilities. One module edited for many reasons means split it; one change touching many modules means consolidate it.
“Single responsibility” is the most argued-about phrase in this profession. Two engineers stare at the same class, one says it has one responsibility, the other says four, and neither can prove it — because “responsibility” sounds like a property you decide in the abstract. It isn’t. There is a way to settle the argument with evidence, and the evidence is already in your repository.
Run git log over a file and watch why it changes. A responsibility is not a noun you assign; it is an axis of change — a reason the file gets edited. When one file gets edited for many unrelated reasons, or one reason forces edits across many files, the git history shows it before any design discussion does. Those two patterns are the two oldest smells in the catalogue, and they are mirror images: divergent change and shotgun surgery.
After this lesson you can define divergent change (one module changed for many reasons → low cohesion → split it) and shotgun surgery (one reason forces edits across many modules → high coupling → consolidate it); diagnose each from co-change patterns in git history rather than from abstract debate; pick the right refactoring direction for each; and recognise the failure mode where a genuine cross-cutting concern is mistaken for shotgun surgery and crushed into a god utility.
Divergent change: one module is edited for many unrelated reasons. When you add a payment provider, this class changes. When the tax rules change, this class changes. When the email template changes, this class changes again. Three different requirement directions, all landing in the same file. That is divergent change — a symptom of low cohesion, because the module bundles responsibilities that vary independently.
// OrderService.ts — touched by three unrelated forces
class OrderService {
charge(order: Order) { /* Stripe API specifics — changes when payments change */ }
computeTax(order: Order) { /* VAT rules — changes when tax law changes */ }
sendReceipt(order: Order) { /* HTML email body — changes when marketing changes */ }
}The tell is in the history: the same file appears in commits whose messages have nothing to do with each other — “switch to Adyen”, “add EU VAT”, “new receipt design”. The fix is to split along the axes of change so each new requirement has one obvious home.
Shotgun surgery: one logical change forces edits across many modules. It is the exact inverse. You decide to add a new currency, and you find yourself editing the cart, the checkout summary, the invoice generator, the receipt email, and the admin export — five files for one conceptual change. That is shotgun surgery — a symptom of high coupling, because one responsibility has been smeared across modules that all hold a fragment of it.
// the SAME rule (how to render money) re-implemented in five places
cart.ts: const label = currency === 'USD' ? '$' + amt : '€' + amt;
checkout.ts: const label = currency === 'USD' ? '$' + amt : '€' + amt;
invoice.ts: const label = currency === 'USD' ? '$' + amt : '€' + amt;
// ...and two moreThe tell is again in the history: a single commit (or, worse, several commits chasing the same bug) touches a scatter of files that should have nothing in common. The fix is to consolidate the scattered fragments behind one boundary, so the next currency is one edit.
These are the empirical SRP smells — you read them off co-change, you don’t argue them. The Single Responsibility Principle is usually stated as “a class should have one reason to change.” That definition is operational, not philosophical: a reason to change is observable. Build a co-change map — for each pair of files (or each file’s set of commit reasons), how often do they change together and why?
// crude co-change signal from history (conceptual)
// git log --name-only --pretty=format:%s → group files by commit
// divergent change: one file, many unrelated commit subjects
// shotgun surgery: one commit subject, many unrelated files- Divergent change = one file, high diversity of change reasons. Cohesion is low; the file is doing several jobs. Split it.
- Shotgun surgery = one change reason, high diversity of files touched. Coupling is high; one job is spread out. Gather it.
This is why the smells beat the abstract debate: two seniors can disagree forever about whether OrderService “has one responsibility”, but they cannot disagree that its history shows three independent change axes. The repository is the referee.
The refactoring direction is opposite for each — and choosing wrong makes it worse. Divergent change wants separation: Extract Class / Move Method until each module varies for one reason. Shotgun surgery wants gathering: Move Method / Inline / Combine until each reason lives in one module. They pull in opposite directions, so the diagnosis must come first.
// divergent change → SPLIT (raise cohesion)
class PaymentGateway { charge(order: Order) {/* … */} }
class TaxCalculator { computeTax(order: Order) {/* … */} }
class ReceiptMailer { send(order: Order) {/* … */} }
// shotgun surgery → CONSOLIDATE (lower coupling)
// money.ts — the one place the "render money" rule lives
export function formatMoney(amt: number, currency: Currency): string { /* … */ }
// cart/checkout/invoice now all call formatMoney(amt, currency)If you “fix” divergent change by gathering, you build a bigger god class. If you “fix” shotgun surgery by splitting, you scatter the responsibility even more. The smell names the cure; that is the whole point of naming them separately.
Read the smell off the log, then pick the cure. Suppose NotificationCenter.ts shows this history:
a1c "support SMS via Twilio"
b2d "new welcome-email copy"
c3e "push notifications for mobile"
d4f "rate-limit SMS to avoid spend spikes"
e5a "redesign email footer"Five commits, three independent reasons (SMS transport, email content, push transport). That is divergent change: one file changing for unrelated forces, low cohesion. The cure is to split by axis of change:
class SmsChannel { send(msg: Message) {/* Twilio + rate limit */} }
class EmailChannel { send(msg: Message) {/* template + footer */} }
class PushChannel { send(msg: Message) {/* mobile push */} }Now the inverse case. You set out to add a “Slack” channel and discover the commit must touch UserSettings.ts (a notifyBySlack flag), OnboardingFlow.ts (a Slack opt-in step), AdminDashboard.ts (a Slack column), and EventDispatcher.ts (a Slack branch) — four files, one conceptual change. That is shotgun surgery: the “which channels exist” decision is smeared across modules. The cure is the opposite — consolidate the channel registry into one place the others consult:
// channels.ts — the single source of truth for "what channels exist"
export const CHANNELS = [SmsChannel, EmailChannel, PushChannel, SlackChannel];
// settings, onboarding, admin, dispatcher all derive from CHANNELS — adding one is one editSame codebase, two different smells, two opposite cures. The log told you which.
▸Why this works
Why trust co-change over reasoning about responsibilities? Because responsibilities-as-nouns are infinitely negotiable — you can always argue a class “really” does one thing at some level of abstraction (“it manages orders”). What you cannot argue away is that the file shows up in commits driven by independent business forces. Co-change is the behavioural definition of coupling and cohesion: things that change together are coupled; a module whose internals change for one reason is cohesive. Tools like code-maps, git log --name-only aggregation, or commit-coupling metrics surface this directly, which is why senior reviewers reach for history when a design argument stalls. The principle is real; the history is just where it becomes measurable.
▸Common mistake
The dangerous failure mode: mistaking a legitimate cross-cutting concern for shotgun surgery and over-centralising. Logging, auth checks, metrics, and tracing genuinely do appear in many modules — that is their nature, not a smell. If you see “logging is touched in 30 files” and react by inventing a CoreUtils god object that every module imports, you have traded a diffuse concern for a single high-fan-in dependency that now couples everything to one volatile file — and every logging tweak becomes its own shotgun surgery. The discriminator: shotgun surgery is one responsibility accidentally scattered (a new currency should be one edit but isn’t). A cross-cutting concern is one aspect that inherently applies everywhere (every handler logs). The right tool for the latter is a thin, stable seam — middleware, a decorator, an aspect, a context — not a god utility that swallows unrelated helpers. Consolidate responsibilities; don’t centralise everything that merely appears often.
A single file appears in commits titled 'add Apple Pay', 'fix VAT rounding', and 'new invoice PDF layout'. Which smell is this, and what is the cure?
Divergent change and shotgun surgery are mirror-image SRP smells you read from git history rather than argue in the abstract. Divergent change is one module edited for many unrelated reasons — low cohesion — and the cure is to split along the axes of change so each requirement has one home. Shotgun surgery is one logical change forcing edits across many modules — high coupling — and the cure is the opposite: consolidate the scattered responsibility behind one boundary so the next instance is a single edit. Because they pull in opposite directions, the diagnosis must come first, and co-change in the log is the diagnostic that beats endless “what is a responsibility” debate. The senior trap is treating a genuine cross-cutting concern — logging, auth, tracing — as shotgun surgery and crushing it into a god utility; those want a thin stable seam (middleware, decorator, context), not centralisation of everything that merely shows up often.
Practice
Start at the top. Tasks go easiest → hardest: recall a fact, apply it to a case, then a senior-level stretch. Open one, attempt it, then reveal.
Something unclear?
Ask a question about this lesson. Questions are anonymous and go straight to the author to make the lesson better.