Appearance
9.3.5 — Single Responsibility
Three people ask for a change to the same class in the same month.
typescript
class Order {
calculateTotal(): Money { /* pricing and tax rules */ } // (1)
saveToDatabase(): Promise<void> { /* SQL */ } // (2)
toInvoicePdf(): Buffer { /* layout, fonts, logo */ } // (3)
}In March, the finance team changes how tax is applied to delivery fees, which is line (1). In the same sprint, the platform team migrates a database column, which is line (2). Also in the same sprint, marketing wants the new logo on invoices, which is line (3).
Three unrelated requests, from three departments, all landing in one file. Three engineers now have merge conflicts with each other. The marketing change — a new logo — cannot ship until the tax change has been reviewed by someone who understands tax, because they are in the same pull request. And the release that ships the logo carries a tax change with it, so if orders start totalling incorrectly, the first suspect is a graphics update.
That is the problem the Single Responsibility Principle names.
1. What it actually says
The popular version is "a class should do one thing", which sounds obvious and helps nobody, because "one thing" can be stretched to any size. Order does one thing: it handles orders.
Robert Martin's sharper wording is the one worth memorising:
A module should have one, and only one, reason to change.
And then the clarification that makes it usable, which he added after years of watching people misapply it:
A reason to change is a person. Gather together the things that change for the same actor, and separate the things that change for different actors.
That reframes the whole principle. Do not ask "does this class do one thing?" — you will argue about it forever. Ask:
Who can ask for this to change?
In the example, the answer is three different departments, so there are three responsibilities in one file. That is not a matter of taste, it is a fact about the organisation, and it is why the merge conflicts happened.
2. The fix
Split by actor, not by size:
typescript
// pricing/order-total.ts — owned by finance's rules
export function calculateTotal(order: Order): Money { /* pricing and tax */ }
// storage/order-repository.ts — owned by whoever owns the database
export class OrderRepository {
async save(order: Order): Promise<void> { /* SQL */ }
}
// documents/invoice-renderer.ts — owned by whoever owns the brand
export function renderInvoice(order: Order): Buffer { /* layout, fonts, logo */ }
// domain/order.ts — the order itself: its data and its own rules
export class Order {
cancel(reason: string): void { /* only the order's own rules */ }
}Four files. The logo change touches one of them, is reviewed by one person, and ships on its own. The tax change touches a different one. Neither can break the other, and neither has to wait for the other.
Notice what Order kept: the rules that belong to an order itself, such as whether it may be cancelled. That is the encapsulation argument from 9.2.2, and it is why the fix is not "move everything out of Order into services", which would produce the anemic model that page warns about. Rules about an order's own state stay with the order. Concerns owned by other departments leave.
3. How to find the responsibilities
Four techniques, in the order they are useful.
One, list who asks. Go through the methods and write the department or role beside each one. Two departments means two responsibilities. This takes five minutes and is the single most reliable method.
Two, read the git history. Look at what has caused this file to change over the last year. If the reasons cluster into groups, the groups are your split lines. This has the advantage of being evidence rather than opinion.
Three, describe it in one sentence without "and". If the honest description is "it calculates totals and saves to the database and renders invoices", each "and" is a seam. The catch is that people cheat by choosing a vaguer word — "it manages orders" — so the sentence must be specific enough to be checkable.
Four, look for the words Manager, Handler, Processor, Util and Helper in class names. These names carry no meaning, which is usually why they were chosen: the class does several things and no honest name covered them. A class that genuinely does one thing can usually be named after that thing.
4. The same principle at four scales
SRP is usually taught at the class level, but it is the same idea everywhere, and recognising it at the larger scales is what makes it useful beyond tidying files.
A function should do one thing at one level of detail. A function that validates, computes and writes to a database has three reasons to change.
A class or module should serve one actor, which is the case above.
A folder or package should hold one area of the business. billing/ should not contain the delivery tracking code, because a change to delivery has no business appearing in a billing pull request.
A service, in the distributed sense, should own one capability and its data. This is where the principle bites hardest, because getting it wrong means two teams deploying together forever. Part 10.8 develops it.
The thread running through all four: the boundary goes where the reasons to change differ. Only the size of the thing changes.
5. When splitting is the wrong move
This principle is the most over-applied of the five, so the limits matter.
Splitting has a real cost: you now open more files to follow one flow. Four small files with one clear flow between them is better than one big file. Four small files where the flow zigzags between them is worse than one, because the reader has to reconstruct an order of operations that no single file shows.
One reason to change is not one method. A class with eight methods that all serve the same actor and protect the same state is perfectly cohesive. Splitting it into eight classes produces eight files that must all be opened together, and it usually forces the shared state out into the open, which converts visible complexity into hidden coupling (9.1 section 6).
Do not split before the second actor appears. If pricing and invoicing are both currently owned by the same two-person team and always change together, they have one reason to change today. Split when the second actor is real, not when it is imaginable — which is YAGNI (9.3.4) applied to structure.
The tell that you have split too far: almost every change touches several of the new files at once. That is the principle's own signal being run backwards — things that change together should live together (9.1 section 3), so if they always move as a group, they were one responsibility all along.
6. Interview calibration
The question is nearly always "what is the Single Responsibility Principle?", and the answer that separates candidates is short.
Weak: "A class should do one thing."
Strong, in about thirty seconds: "One reason to change, where a reason means a person or a department who can ask for it. So I look at a class and ask who could request a change to each method. If finance can change the tax calculation and marketing can change the invoice layout, that class has two masters and their work will collide in the same file and the same release. The fix is to split by actor rather than by size. And the limit is that splitting costs indirection — if all the pieces always change together, they were one responsibility, and I would leave them alone."
The common follow-up is "how do you decide where to split?" The best answer names evidence rather than intuition: list the actors, and read the git history for what has actually caused this file to change.
Recall
- The precise wording is one reason to change, and Robert Martin's clarification is that a reason to change is a person: gather what changes for the same actor, separate what changes for different actors. So the question is "who can ask for this to change?", not "does it do one thing?".
- The failure it prevents: finance, platform and marketing all editing one file in one sprint, so unrelated changes collide, wait for each other's reviews, and ship in the same release.
- Split by actor, not by size. Rules about an object's own state stay with the object — moving everything out produces the anemic model from 9.2.2.
- Finding responsibilities: list who asks for each method; read the git history for what has actually caused changes; describe the class in one sentence and treat each "and" as a seam; treat
Manager,Handler,ProcessorandUtilnames as a signal that no honest name covered it. - Same idea at four scales: function (one thing, one level of detail), class (one actor), folder (one business area), service (one capability and its data).
- Most over-applied of the five. Splitting costs indirection. Eight methods serving one actor are cohesive, not eight responsibilities. The signal you went too far: almost every change touches several of the new files at once.
Self-test: State SRP without the phrase "one thing". What does "actor" mean here, and why does it make the principle checkable? Name two evidence-based ways to find the split lines. What stays inside the class when you split, and why? What is the tell that you split too far?
Quiz Bank
InterviewWhat is the Single Responsibility Principle, and how do you decide where the boundary goes?
The statement: a module should have one, and only one, reason to change. The clarification that makes it usable is that a reason to change is a person — gather the things that change for the same actor, separate the things that change for different actors.
Why the popular phrasing fails. "A class should do one thing" cannot be checked, because "one thing" stretches to fit whatever you already wrote. OrderManager does one thing: it manages orders. The actor version cannot be stretched, because the actors are real people in a real organisation, and you can go and count them.
Where the boundary goes, using evidence rather than taste. First, list who can request a change to each method — if the tax calculation answers to finance and the invoice layout answers to marketing, that is two responsibilities in one file, and the proof is that their pull requests will conflict. Second, read the git history: the reasons this file has actually changed over the past year usually cluster, and the clusters are the split lines. Both of these are observations, which is why they end arguments that intuition cannot.
What stays behind. Rules about the object's own state stay with the object — whether an order may be cancelled belongs on Order. Pulling everything out into services is the anemic domain model, which trades one problem for a worse one.
The limit, which a complete answer includes. Splitting costs indirection: more files to open to follow one flow. A class whose eight methods all serve one actor and guard the same state is cohesive, not overloaded. And the signal that you split too far is that nearly every change now touches several of the new files together — which means they shared a reason to change all along.
AppliedYou find a 2,000-line OrderService with 22 injected dependencies. Walk through how you would break it up.
First, resist the instinct to split by size. Cutting a 2,000-line class into four 500-line classes achieves nothing if the four still change together and still share state. The split has to follow the reasons for change, or you have just made the flow harder to follow.
Step one: map methods to actors. Go down the method list and write beside each one who can request a change to it. In practice a class this size usually resolves into three or four groups — the order's own rules, payment concerns, fulfilment and delivery, and reporting or notifications. That mapping is the design, and it takes half an hour.
Step two: check the map against the git history. Look at what has actually caused this file to change over the last year. Where history and the actor map agree, you have a confident boundary. Where they disagree, history usually wins, because it is evidence.
Step three: push the domain rules down before extracting anything. A service this large is usually large because the domain objects are empty. Rules like "an order can only be cancelled before dispatch" belong on Order, and moving each one deletes a branch from the service and often a dependency with it. Do this first, because it shrinks the problem before you start drawing lines, and because the twenty-two dependencies are a symptom of it.
Step four: extract one group at a time, shipping each separately. Not one big pull request. One group, reviewed and released on its own, so any regression is attributable to a small change. Start with the group that changes most often, because that is where the benefit lands soonest and where the team will feel it.
Step five: know what "done" looks like, so the work has an end. Each remaining class serves one actor. A typical change touches one of them. Test setup no longer needs twenty-two fakes, which is the measurable proxy for the coupling having dropped.
What I would not do: split methods that all guard the same state just to hit a line count, and touch any group that has not changed in two years — it is not costing anything, and moving it is risk with no return.