DecompositionCONTESTEDSCALE-SPECIFICPARADIGM-SPECIFIC

Single Responsibility, Carefully

Not "a class does one thing" — that phrasing has no content. "A module should have one coherent reason to change" has some, and even then the hard cases are genuinely ambiguous.

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

What does "single responsibility" actually mean, and how do I use it without shattering a codebase into pieces nobody can follow?

The requirement

A reviewer writes "this violates SRP" on a 90-line class. The author asks which responsibility should move. Neither of them can answer without inventing a rule on the spot.

The obvious build

A class should do one thing. If you can describe it with the word "and", split it. This is memorable, it fits on a slide, and it gives a junior engineer something to apply immediately, which is why it spread.

Why it breaks

"One thing" has no fixed granularity. OrderService does one thing (orders). saveOrder does one thing (saving). setStatus does one thing. Every unit at every size passes and fails the test depending on how you phrase it, which makes it unfalsifiable (Changeability Is the Goal).

How it breaks as requirements change
  • "One thing" has no fixed granularity. OrderService does one thing (orders). saveOrder does one thing (saving). setStatus does one thing. Every unit at every size passes and fails the test depending on how you phrase it, which makes it unfalsifiable (Changeability Is the Goal).
  • Taken literally it produces OrderValidator, OrderPersister, OrderNotifier, OrderStatusUpdater — four classes, four files, one coordinating class that calls all four, and a reader who now needs five files to answer a question that used to take one (Local Reasoning).
  • It says nothing about which splits are *worth* making, so it fires equally on the god object that is genuinely costing the team and on the 90-line class that has never caused a problem.
  • Uncle Bob's own later formulation — "gather things that change for the same reason, separate things that change for different reasons" — is a materially different claim from the slide, and most people repeating the principle have only met the slide.
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 reviewer is not wrong that the class feels overloaded, and the author is not wrong that no proposed split is obviously better.
  • Whatever is agreed becomes precedent for the next hundred reviews on this team.
  • The team has previously over-applied this and has a directory of one-method classes it now regrets, so the conversation is not neutral.
Invariants
  • Whatever the split, an invariant that was enforced by one unit must still be enforced by exactly one unit afterwards — never by a convention spanning two (Enforcing Invariants).

Who owns what, and where the seams fall

Responsibilities decide boundaries; boundaries decide what an interface has to say.

Responsibilities
  • The principle owns one job: telling you when two things in a unit answer to different people, so a change requested by one forces a redeploy and regression for the other.
  • It owns nothing about file size, method count, or how many verbs are in a name.
  • The engineer, not the principle, owns deciding whether the split is worth its indirection (The Cost of Change).
Boundaries
  • The usable boundary test is external: who asks for this change? Two pieces of code with the same requester and the same release cadence belong together, whatever their verbs are.
  • That test is falsifiable — you can be shown to be wrong about who asks — which is exactly what "one thing" lacks.
  • It fails in-house on internal technical concerns, where the requester is always "engineering". For those, fall back to rate of change (Finding Seams).

Three statements, one of which you can act on

CONTESTEDA serious minority position holds that even the third form is post-hoc — you only know which changes were independent after they arrive, so the trigger analysis at design time is still a prediction dressed as evidence. The counter is that for existing code the commit log is real evidence, which is precisely why this principle is far more reliable applied to a module with two years of history than to one being written today.

The principle exists in at least three forms in circulation, and they are not restatements of each other. Knowing which one is being invoked is most of what makes the review conversation productive.

Only the third is falsifiable, and it is falsifiable in a specific way: you can go and look at the last twenty commits to the file and see whether they came from one source or several. That turns an aesthetic argument into a question with an answer.

StatementWhat it tells youWhy it failsVerdict
"A class should do one thing"Nothing, without a granularityEvery unit does one thing at some level of description and many at another. Unfalsifiable.Do not use it. It is the version that produced the one-method-class directories.
"A class should have one reason to change"Closer — points at change rather than activityStill slippery: "a reason" can be sliced arbitrarily fine, so it can justify almost any split after the fact.Usable as a prompt, not as a verdict.
"Gather what changes for the same reason; separate what changes for different reasons — where the reason is a person or team who asks"Which changes are independent, and therefore which splits contain anythingGoes blank on purely technical modules where the requester is always engineering.The version worth teaching. Check it against the commit log, not against intuition.

The cases where competent people disagree

These are not trick questions. Each has been argued in real code reviews between engineers who were both right about something, and in each the disagreement is about a prediction rather than about the principle.

The point of listing them is not to resolve them. It is that ambiguity here is the normal case, not a failure of understanding — and a team that recognises an ambiguous case can decide it with a revisit trigger and move on, instead of relitigating it every sprint.

Four splits reasonable engineers argue about

Does this unit have one coherent reason to change, or several?

Validation and persistence in one entity

when The Order type validates its own fields and knows how to save itself.

cost Split camp: the schema changes for engineering reasons and the rules change for business reasons — two triggers. Keep camp: in this codebase both have only ever changed together during a migration, so splitting adds an interface and contains nothing. Both are correct about their own codebase; check the commit log to find out which one you are in.

Formatting inside a domain type

when Money.format(locale) sits next to Money.add.

cost Split camp: presentation changes when designers ask and arithmetic changes when finance asks. Keep camp: the alternative is a formatter that must know every internal detail of Money, so you have traded a cohesion problem for an encapsulation one (Feature Envy). This one usually resolves toward keeping it, and the reason is that the split leaks more than it contains.

Reads and writes in one repository

when One class with findById and save.

cost Split camp: query needs and write needs diverge and eventually pull in opposite directions (the CQRS argument). Keep camp: for most systems they never diverge, and the split doubles the surface for zero observed benefit. Splitting early here is one of the most common instances of speculative generality in the field (Speculative Generality).

A handler that validates, authorises and dispatches

when Every HTTP handler does the same three steps inline.

cost Split camp: authorisation is a security trigger and must be reviewable in one place (Where the Check Belongs). Keep camp: three lines in the handler is more locally readable than three collaborators. Here the security argument usually wins, and it wins on blast radius rather than on SRP — which is a good illustration that the strongest reason to split is often not this principle at all.

A 900-line generated-protocol codec

when One module, hundreds of methods, one reason to change.

cost There is no real disagreement once the trigger list is written: one trigger, do not split. It is on this list because it is the case where "one thing" and "one reason to change" give opposite answers, which is the clearest available demonstration that they are different principles.

When the split makes the next change more expensive

Arguments for this principle almost always price a change that the split contains. The honest version also prices one that the split spreads out, because those exist and they are how over-application does its damage.

The case below is deliberately unflattering: a textbook-correct SRP split, and a requirement that lands across all four pieces because the four pieces were cut along verbs rather than along triggers.

A requirement that crosses a textbook split
The change

Orders placed by enterprise customers skip payment capture, defer to invoicing, and must be visible in the admin list as "awaiting PO" — one requirement, one requester.

Split by verb: OrderValidator, OrderPersister, OrderCharger, OrderNotifier, plus an OrderCoordinator
OrderValidatorOrderPersisterOrderChargerOrderNotifierOrderCoordinatorAdminOrderView
testsvalidator_testpersister_testcharger_testnotifier_testcoordinator_test (mock-heavy)admin_view_test
6 modules · 6 test files

Every piece needs to know about the new customer type, because the split was along activities and the requirement is not about an activity. The coordinator test is the worst of it: it asserts the order in which four mocks are called, so adding a conditional branch rewrites it entirely.

Split by trigger: an order lifecycle that owns the states, a payment module behind a port, and one handler
order/lifecycle.tsorder/payment-port.tsAdminOrderView
testslifecycle_testpayment_port_contract_testadmin_view_test
3 modules · 3 test files

Enterprise orders are a new path through one state machine and a second implementation of one port. Nothing about validation or notification is opened, because nothing about them changed.

what it cost The second design is not free and is not universally better. It puts more logic in one module, so a reader wanting only the persistence detail has more to skim; it requires knowing the state machine before making any change at all; and it is worse than the first design for a requirement that genuinely is about one activity — "notifications must go through the new vendor" is a one-file change under the verb split and a port-plus-adapter exercise under this one. The general point stands: a split is a bet on which axis change arrives along, and the verb axis is almost never that axis (Choosing the Model).

How to build it

Most important first.

  • Restate the claim before applying it: a module should have one coherent reason to change. Coherent is doing work in that sentence — several changes with one trigger are one reason.
  • Enumerate triggers, not activities. Write the actual list: security review, marketing copy, tax law, vendor swap. If the list has one entry, stop, whatever the file size (Designing by Responsibility).
  • Ask what a split costs before making it. Splitting always adds an interface, a name and a hop, and buys containment only if the triggers genuinely fire independently (What an Abstraction Costs).
  • Prefer moving one volatile responsibility out over partitioning the unit evenly. Even partitions are a sign the analysis was about size.
  • Accept ambiguity. In the genuinely unclear cases below, the right answer is a decision plus a revisit trigger, not a principle (Revisit Triggers).
  • Say "these two things change for different reasons, here they are" in review. Never say "SRP violation" — it is unarguable, which is its main appeal and its main defect (Tone, Disagreement and Receiving Review).

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
  • Applied where triggers genuinely differ: the next marketing copy change touches one small module and one test, instead of redeploying the code that handles payments.
  • Applied where triggers are the same: the next change touches three files instead of one, plus the coordinator, and the test suite grows a mock for something that was a function call. That is a permanent tax with no return, and it is the more common outcome on teams that treat the principle as a rule.
  • The asymmetry matters when deciding: an under-split module can be split later using the evidence of what actually changed, while an over-split one is rarely merged back because merging looks like regression to whoever reviews it (Reversible and Irreversible Decisions).
What the recommended approach costs
  • The "reason to change" formulation is much better than "one thing" and still requires judgement, so it cannot settle a disagreement between two engineers who predict different futures.
  • Being conservative about splitting means occasionally living with a module that should have been split, and paying the retrofit later under pressure.
  • The external-trigger test is the most falsifiable version available and it goes blank on purely technical modules, where you are back to intuition about rate of change.

What can go wrong

Failure modes
  • Over-application: a class per verb, a coordinator that does nothing but call them in order, and a codebase where every feature is a scavenger hunt (Over-Decomposition).
  • Under-application: the trigger list has seven entries, everyone can see it, and nobody moves because the class is "not that big" (God Object).
  • The principle used as a review weapon: unarguable, ungrounded, and corrosive because the author cannot respond with evidence (What Code Review Is For).
  • The mitigation fails in its own way: "list the triggers" turns into a ritual where the list is written to justify a split already decided, which is how any heuristic dies.
Dependencies, and their direction
  • Every split creates a dependency where there was a method call, and dependencies have direction. A badly directed split — validation depending on persistence — is worse than no split (Dependency Direction).
  • The principle depends on being able to predict future change, which is exactly what nobody can do reliably. That dependency is the honest limit on how much weight it can carry (Design for the Known, Name What You Assumed).
Misreads
  • "SRP means small classes." It says nothing about size. A 900-line protocol codec with one reason to change satisfies it; a 30-line class that changes for four reasons does not (Long Functions).
  • "SRP means one public method." That is a different and much worse idea, and it is where the DoSomethingCommandHandler directory comes from (How SOLID Gets Misused).
  • "If two things are in the same file, that is a violation." The principle is about reasons to change, not about files. Two things that always change together should share a file (Cohesion).
  • "It is a principle, so it is not negotiable." Every SOLID principle is a heuristic with a cost, and this one is the most frequently applied without paying attention to that cost (Single Responsibility, Critically).
Smells this explains
  • divergent-change
  • god-object

Testing it, and how it ages

What to test, and at which boundary
  • A good split shows up in the tests immediately: each piece becomes testable without constructing the others. If the tests still need all four objects, the split was file-level only (Testing as Design Feedback).
  • A bad split also shows up: the coordinator's test becomes a mock-orchestration exercise that asserts call order rather than behaviour (Mocking).
  • Test the invariant after the split, at whichever unit now owns it, and delete the old test only when the new one covers the same case.
How this design ages
  • Reasons to change are discovered, not predicted. A module with one trigger today can acquire a second when the company enters a regulated market, and the split becomes obvious in retrospect — which is an argument for splitting late rather than early.
  • The principle ages worst on stable code: a module nobody has touched in three years is not improved by being split, and the split costs the review time of everyone who reads it afterwards.
  • It ages best on the hot paths — the two or three modules a team edits weekly — which is where all of this domain's effort should concentrate (Change Amplification).

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 view is not "big classes are fine" but that SRP is unfalsifiable and therefore harmful: because "responsibility" can be sliced at any granularity, the principle ratifies whatever split its user already preferred, and its most enthusiastic application reliably produces one-method classes, mock-heavy tests and worse local reasoning than the code it replaced. Engineers who hold this view point at real codebases that got worse. The defence offered here is narrow — replace "one thing" with "one external trigger for change", which can be checked against who actually files the tickets — and it concedes that the slide version deserves the criticism.
  • SCALE-SPECIFICAt one team owning everything, "who asks for this change" is nearly always the same person, so the trigger test goes blank and rate of change is all you have. At twenty teams the trigger test is sharp, because a marketing change and a security change come from different orgs on different schedules and the coordination cost of them sharing a file is measurable in calendar time.
  • PARADIGM-SPECIFICIn OO the debate is about classes and it is entangled with where state lives. In a functional codebase the same question is about modules and the data types they operate over, and it is easier: functions that change for different reasons can sit in one file with no coupling penalty, because they share no mutable state, so the pressure to split is much lower and the principle correspondingly less useful.

Where the depth lives

This domain teaches the codebase-level structure and hands the rest off.

Architecturemodular-monolith
Domains that do not exist yet
  • Testing & Reliability Engineering — mock-heavy coordinator tests are the most reliable early signal that a split was made along the wrong axis, and they show up long before the expensive requirement does.