Single Responsibility, Critically
"A coherent reason to change" is the useful reading. "One thing" is the one that produces a hundred classes that each do nothing.
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.
What counts as one responsibility, when every class can be described as doing one thing or as doing five?
InvoiceService calculates invoice totals, renders a PDF, emails it, and writes the record. Finance change the tax rules, design change the PDF layout, and marketing change the email copy. All three teams edit the same file, and every release something unrelated breaks.
Leave it as one service. Everything about invoices is in one file, so a new engineer can read the whole story top to bottom, and there is no indirection between the calculation and the output. For a small system this is genuinely the most readable arrangement, and splitting it early would have been over-design (When Design Does Not Pay).
The three teams collide. Not occasionally — structurally, because every one of them must edit the same file to do their job, which is a coordination cost that grows with headcount and never with the code (Divergent Change).
- The three teams collide. Not occasionally — structurally, because every one of them must edit the same file to do their job, which is a coordination cost that grows with headcount and never with the code (Divergent Change).
- The audit trail is polluted: the tax calculation's commit history is interleaved with email copy tweaks, so "when did this rule change" takes an hour instead of a minute.
- Testing the tax rules requires a PDF renderer and a mail transport on the path, so those tests are slow and get skipped (Testing as Design Feedback).
- Blast radius: a layout change has twice shipped a total change, because both live in one class and the tests that would have caught it were the slow ones.
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.
- Three teams with three release cadences edit this file today; that is the actual pain and any design has to address it.
- The tax calculation is legally auditable, so its change history must be readable without PDF layout commits in the way.
- The team has previously split a class into eleven and hated it, so "split it up" is not an argument that will land on its own.
- The invoice record write and the total calculation must stay transactionally consistent, which constrains how far apart they can move.
- The number written on the PDF, the number in the email and the number in the database are the same number, however the code is arranged.
- A change to PDF layout can never change a total. Today nothing enforces that, which is why it has happened twice.
Who owns what, and where the seams fall
Responsibilities decide boundaries; boundaries decide what an interface has to say.
- Invoice calculation owns the numbers, the tax rules and their audit trail. It is the piece with a legal obligation and it should be boring, pure and heavily tested.
- Invoice rendering owns turning a calculated invoice into a document. It changes when design changes and never when tax law does.
- Delivery owns getting a rendered invoice to a customer — email today, a portal download tomorrow.
- Persistence owns the record. It stays close to calculation because of the transactional invariant, which is a case of a constraint overriding a clean split (Consistency Boundaries).
- The seams fall on the lines between the three teams, because "who asks for the change" turns out to be the most reliable operational meaning of "reason to change" — this is why Martin's later restatement uses "actor" rather than "reason".
- The seam does *not* fall between calculation and persistence, even though a naive split would put it there. The invariant binds them, and boundaries that cut across an invariant produce the worst kind of coupling (Where Invariants Live).
- Four pieces, not eleven. The stopping rule is that each resulting unit still does something a stakeholder would recognise as a thing (Over-Decomposition).
Count the reasons, do not argue about them
The word "responsibility" is where every SRP argument dies, because both readings of any class are defensible. The way out is to stop arguing about the noun and look at the history: what has actually changed in this unit, and who asked for it. That is observable, it is in git log, and it settles the question in a way no definition has managed to.
Laid out as a responsibility analysis, this class does not need an opinion to convict it. Four independent reasons to change, three of them belonging to different teams, is the finding.
- — Tax rules by jurisdiction and product category
- — The PDF layout: fonts, margins, where the logo goes
- — The SMTP configuration and the email template
- — The invoices table schema and the transaction boundary
- — Computes line totals, tax and grand total
- — Renders a PDF document
- — Sends an email with the PDF attached
- — Writes the invoice record and commits
- — A tax rate table
- — A PDF library
- — An SMTP client
- — The database connection
- — Finance change a tax rule (audited, roughly monthly)
- — Design change the invoice layout (roughly quarterly)
- — Marketing change the email copy (roughly monthly)
- — A schema migration changes the invoice table (rarely)
- — The PDF library has a breaking release (rarely, on someone else's schedule)
Five reasons to change, driven by three different teams plus an external vendor. This is not a judgement call about what "one thing" means — three groups of people with three cadences are structurally required to edit one file, and that is a merge point and a regression source by construction. Split by requester into calculation, rendering and delivery; keep the record write next to calculation because a transactional invariant binds them (Consistency Boundaries).
The misuse: splitting until nothing does anything
The failure mode is not theoretical and it is more common than the god object it was meant to prevent. Applied as "one class, one thing", SRP has no stopping rule, so teams keep going: a class per method, then a class per step, then interfaces for each, and the invoice logic ends up distributed across eleven files that each forward to the next.
The tell is that no single file answers a question anyone has. Understanding what an invoice total is requires opening every file in the chain, and each one contributes two lines. The knowledge did not get organised; it got scattered, and this is a worse outcome than the original class (Over-Decomposition).
invoice/ InvoiceTotalCalculator.ts // calls the next three LineSubtotalCalculator.ts // sum(qty * price) TaxCalculator.ts // calls TaxRateResolver TaxRateResolver.ts // reads a table DiscountApplier.ts // one if statement RoundingStrategy.ts // Math.round, behind an interface InvoiceTotalCalculatorFactory.ts // "What is the tax on a German B2B order?" // -> open five files, none of which contains the rule, // each of which contains one line of it.
invoice/
calculation/ // finance changes this. Pure, audited, no I/O.
total.ts // ~150 lines: lines, discounts, tax, rounding.
rates.ts // the rate table, versioned by effective date.
rendering/ // design changes this.
pdf.ts
delivery/ // marketing changes this.
email.ts
// "What is the tax on a German B2B order?"
// -> one file, read it top to bottom.The right-hand version has fewer, larger units and answers the question in one place, which is the property that actually matters — a reader forms a complete picture without navigation. The left-hand version optimised for a metric (methods per class) that nobody was paying, while making the thing people do daily (understand a total) strictly more expensive. Both satisfy "one thing" under some description; only one of them makes a change cheaper, and change cost is the only judge this domain recognises (The Cost of Change).
The five-part reading
Stated the way this module states every principle: the problem it addresses, an example where it genuinely earns its cost, the misuse, the trade-off, and a real design that ignores it and is right to.
The counterexample matters most, because it is the part checklists never include, and because anyone who has worked in a well-built system has seen one.
- Problem it addresses — a unit that several stakeholders must edit for unrelated reasons is a permanent merge point, a regression source and an unreadable change history.
- Useful example — the invoice split above: three teams, three cadences, one file, and a layout change that twice altered a total.
- Misuse — "one class, one thing" applied with no stopping rule, producing eleven files that each hold one line of a rule (Over-Decomposition).
- Trade-off — every split makes independent changes cheaper and additive changes more expensive, and couples the code structure to an organisation chart that will change.
- Counterexample — a well-written parser, a
Moneytype, a date library: single-file, wide-surfaced, dozens of methods, one stakeholder, changed rarely and coherently. Splitting them by "one thing" would produce nothing but navigation (What an Abstraction Actually Is).
looks like A class whose every method is one line: return this.next.doThing(x). Often named ...Manager, ...Handler or ...Coordinator, often introduced during a split, often with an interface of its own.
suggests A decomposition that went past the point of usefulness. The class adds a name and a file and no behaviour, so every reader pays navigation cost for nothing, and the "responsibility" it holds cannot be described without using the word "coordinates" (Over-Decomposition).
fix Inline it and call the target directly. If several such classes form a chain, collapse the chain and see what is left; usually one module with a real interface. Before deleting, check it is not one of the four legitimate cases above — the distinguishing question is whether anything would be *lost* other than a hop.
How to build it
Most important first.
- List the changes this file has actually received in the last year and who asked for each. This takes twenty minutes with
git logand replaces an entire category of argument with evidence. - Group the changes by requester. Each group is a candidate responsibility, and the groups are visible rather than debatable.
- Split along those groups, and stop. Resist splitting further along technical lines nobody has ever changed independently.
- Keep pieces together where an invariant binds them, even when the split would look tidier. An invariant that spans two units has to be enforced by coordination, which is worse than a slightly wide unit (Enforcing Invariants).
- Check the result against the actual pain: can the three teams now work without touching each other's files? If not, the split was cosmetic (Change Amplification).
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: any of the three teams' changes costs one edit to a shared file, a full invoice regression suite, and a merge negotiation if two land in the same week. The regression suite is slow because it renders PDFs.
- After: a tax change costs one edit to a pure module and a fast test run, with an audit-clean commit history. A layout change cannot alter a total, because it cannot reach the code that computes one.
- What got more expensive, and it is not nothing: adding a field that flows from calculation through rendering to email now touches three modules and a data type, where before it touched one file. Additive changes get worse; independent changes get better (Change Amplification).
- The next *structural* change — a second delivery channel — is now local to delivery, which is the change this split was actually bought for.
- Four modules means four files, four names, and a sequencing step that did not exist. Reading the whole invoice story now requires navigation that reading one file did not (Local Reasoning).
- Splitting by requester couples the code structure to the organisation chart, which changes. A reorg can make yesterday's perfect split arbitrary — a real and underappreciated risk (Code Ownership).
- The additive-change tax is permanent, and on a codebase whose changes are mostly additive it can exceed the benefit.
What can go wrong
- The split is done by technical layer instead of by requester —
InvoiceCalculator,InvoiceMapper,InvoiceValidator,InvoiceHelper— and the three teams still all edit all four (Decomposition by Folder). - Over-splitting: eleven classes, each with one method, each calling the next, and understanding an invoice now requires opening all eleven (Over-Decomposition).
- The mitigation fails when a coordinating service quietly absorbs the logic that was supposed to be distributed, so the god object moves rather than disappears (God Object).
- A shared
InvoiceUtilsappears to hold the bits that did not obviously belong anywhere, and becomes the new collision point (The Utility Dumping Ground).
- Rendering depends on the calculated invoice — a data type — and not on the calculator, so the tax rules can be changed without recompiling the renderer.
- Delivery depends on rendering's output, again as data. The chain is linear and points from volatile presentation toward stable calculation (Dependency Direction).
- An application service depends on all four and sequences them. It has high fan-out and no logic, which is the correct shape for a sequencer.
- "A class should do one thing." The most damaging sentence in this area. "One thing" is scale-free — an invoice system does one thing, and so does a getter — so the rule licenses splitting to any depth, and people take it to the depth where nothing does anything (Over-Decomposition).
- "One reason to change means one method." A class with fifteen methods that all change when tax law changes has one reason to change and is correct. Method count is not the measurement (Long Functions makes the same argument about lines).
- "So we should split this class." Maybe. First check whether it has actually received changes for different reasons. Many wide-looking classes have a single stakeholder and have never been a collision point, and splitting them is pure cost (YAGNI, With Its Bill Attached).
- "SRP means small classes." It means coherent ones. A small class that changes for three reasons violates it; a large one that changes for one does not (Cohesion).
- divergent-change
- god-object
- utility-dumping-ground
Testing it, and how it ages
- Calculation gets pure, fast, thorough tests, including the historical cases the auditors care about. This is where the test budget should go and the split is what makes it possible (What a Unit Is).
- Rendering gets snapshot or golden-file tests, which are cheap and catch layout regressions without touching tax rules (Characterization Tests).
- One integration test that the number on the PDF equals the number in the database, because that is the invariant and it now spans modules.
- Do not write a test per extracted class reflexively. A test suite that mirrors the class structure is a test suite that will break on the next refactor without catching anything (Mocking).
- Calculation grows internal structure — tax strategies, rounding policies — and stays one responsibility from the outside. Growth inside a boundary is success, not drift.
- Delivery is the piece most likely to multiply: email, portal, webhook, print. That is the anticipated axis and it is where the split pays a second time.
- The design stops fitting if invoices diverge by product line to the point that "an invoice" is no longer one concept. Then the split is along the wrong axis and should be by product line, with each owning its own calculation and rendering (Vertical Slices).
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.
- CONTESTEDThe strongest opposing case: "reason to change" is not observable in advance, so in practice SRP licenses whatever split the speaker already preferred, and the empirical result across many codebases is more classes, more indirection and no measurable reduction in change cost. Critics with large-system experience argue that a smaller number of larger, cohesive modules with deep interfaces is cheaper to work in than many shallow ones — Ousterhout's "deep modules" argument is the best-developed version, and it explicitly identifies aggressive decomposition as a leading cause of complexity. That argument is strong and this lesson concedes most of it; what survives is the narrow, checkable version: when two different stakeholders have repeatedly edited the same unit for unrelated reasons, that is observable history rather than speculation, and it is worth acting on.
- PARADIGM-SPECIFICIn a functional codebase SRP is mostly what a function already is, and the interesting unit is the module: the same question becomes "does this module's public surface serve one purpose". The class-splitting mechanics do not transfer, and applying them produces single-function modules with no benefit. In a data-oriented or ECS design the axis is inverted entirely — behaviour is grouped by system and data by component, and "one class one responsibility" is not a meaningful statement about either.
Where the depth lives
This domain teaches the codebase-level structure and hands the rest off.
- — Testing & Reliability Engineering — "these tests need a PDF renderer to check a tax rule" is how this problem announces itself long before anyone names a principle.