Introduction to the Single Responsibility Principle
Single Responsibility Principle (SRP)
SRP works by aligning each module with exactly one source of change — one group of people (an “actor”) who can request modifications — so that a request from one stakeholder can never force an edit that risks breaking another stakeholder's behavior.
Robert C. Martin's precise statement is the one to memorize, because the popular paraphrases are wrong:
“A module should have one, and only one, reason to change.” — and a “reason to change” means one actor: a single source of requirements (a role, a department, a stakeholder).
Note what SRP is not saying. It is not “a class should do only one thing” — a class can have many methods. It is not about how many features a tool has (the old multitool-vs-knife analogy conflates “does many things” with “serves many masters”; a Swiss Army knife with one owner has one reason to change). SRP is about who can demand a change. A class with twenty methods all answerable to a single team is fine; a class with two methods answerable to two different teams is a violation.
A concrete violation: the classic Employee class
Martin's own example. One class accidentally serves three different actors. Watch how a change requested by one of them silently breaks another.
// VIOLATION — three actors, one class
class Employee {
double calculatePay() { /* used by Finance */ }
String reportHours() { /* used by HR / Ops */ }
void save() { /* used by DBAs / Tech */ }
// helper shared by calculatePay() AND reportHours()
private double regularHours() { /* ... */ }
}The three public methods answer to three distinct actors with three distinct “reasons to change”:
| Method | Actor (who asks for changes) | Example change request |
|---|---|---|
calculatePay() | Finance / CFO | “Overtime starts after 40h, not 45h.” |
reportHours() | HR / Operations COO | “Round reported hours up to the nearest 15 min.” |
save() | DBAs / Tech lead | “Switch from Postgres to a new schema.” |
The failure traced step by step
Both calculatePay() and reportHours() call the shared private helper regularHours(). Trace a single Finance request with real numbers:
- Start state. An employee works 45 hours.
regularHours()returns 40 (capping regular hours at 40). Finance pays40 × $50 + 5 × $75 = $2,375. HR'sreportHours()reports 40 regular hours to Operations. Both are correct today. - Finance files a request. CFO: “Treat the regular-hours cap as 45 for a new salaried tier.” A developer edits the shared
regularHours()to return 45. Pay recomputes to45 × $50 = $2,250. Finance is happy. Tests for pay pass. - The silent break.
reportHours()also callsregularHours(). It now reports 45 regular hours to Operations — but HR never asked for this and has no test guarding it. Operations' overtime dashboards, headcount-planning, and compliance reports are now wrong, and nobody noticed because the change was filed and reviewed as a pay change. - Why it's insidious. Two actors were coupled through one method. The reviewer who approved a Finance PR had no reason to think about HR. This is exactly the “unexpected duplication / accidental coupling” SRP exists to prevent.
The fix and why the naive version was wrong
The naive class was wrong not because it “did three things” but because it forced three actors to share one editable surface — the private helper became a hidden coupling channel. Separate the three responsibilities so each class answers to exactly one actor, and let them share immutable data only:
// FIX — each class has ONE reason to change
final class EmployeeData { // plain immutable data, no behavior
final int id;
final double hourlyRate;
final int hoursWorked;
EmployeeData(int id, double hourlyRate, int hoursWorked) {
this.id = id; this.hourlyRate = hourlyRate; this.hoursWorked = hoursWorked;
}
}
class PayCalculator { // changes only when FINANCE asks
private static final int REGULAR_CAP = 40;
double calculatePay(EmployeeData e) {
int regular = Math.min(e.hoursWorked, REGULAR_CAP);
int overtime = Math.max(0, e.hoursWorked - REGULAR_CAP);
return regular * e.hourlyRate + overtime * e.hourlyRate * 1.5;
}
}
class HourReporter { // changes only when HR / OPS asks
private static final int REGULAR_CAP = 40;
int reportHours(EmployeeData e) { return Math.min(e.hoursWorked, REGULAR_CAP); }
}
class EmployeeRepository { // changes only when DBAs / TECH ask
void save(EmployeeData e) { /* persist */ }
}Now the Finance request from the trace edits PayCalculator.REGULAR_CAP only. HourReporter is a different file, owned by a different team, with its own cap and its own tests. The leak path is gone. (The two caps look duplicated — but that is intentional: they are coincidentally equal today and change for different reasons. Collapsing them back into one shared constant would re-introduce the exact coupling we just removed. SRP deliberately tolerates this kind of duplication.)
Pitfalls
- The “one thing” trap → over-splitting. Reading SRP as “one method per class” explodes the design into hundreds of anemic one-method classes. You trade accidental coupling for navigational chaos and dependency wiring overhead. The test is the actor, not the verb count.
- Over-eager DRY. The single most common SRP violation in real code is “helpfully” extracting a shared helper used by two actors — exactly the
regularHours()mistake. Two pieces of code that look identical but answer to different stakeholders must stay separate. DRY applies to one reason-to-change, not across them. - Confusing layers with actors. Splitting by technical layer (controller / service / repo) is orthogonal to SRP. A single “service” can still serve Finance and HR. Ask who files the change ticket, not which tier the code sits in.
- Vague responsibility names. If you can only describe a class's responsibility with “and” (“it manages users and sends email”) or with a god-word like
Manager/Processor/Helper, it is almost certainly serving multiple actors. - Anemic-domain over-correction. Pushing all behavior out of data objects so they hold only fields can scatter logic that genuinely belongs together. SRP separates by reason-to-change, not by “data here, behavior there” dogma.
When to apply SRP — and when not to
Decision signals that point you here: a class is edited by PRs from two different teams; a single “reason” change keeps forcing merge conflicts; you struggle to name the class without “and”; the same file appears in commits tagged for unrelated features; a bug in one feature surfaces in an unrelated one (a coupling smell).
When NOT to split aggressively: early-stage / exploratory code where the actors aren't known yet, small scripts, or stable code that genuinely has one owner. Premature splitting locks in boundaries you'll guess wrong — and the wrong boundary is more expensive than a temporarily fat class.
SRP vs. alternatives
- vs. a single cohesive class (no split). Gain: changes from different actors stop colliding; reviews stay scoped; blast radius shrinks. Cost: more classes, more files to navigate, and orchestration code (a
Facadeor service) to recombine the pieces — plus tolerated duplication. Choose SRP when two or more distinct actors edit the same code; keep one class when a single owner controls all of it and the methods are cohesive. - vs. the God Object (the natural drift). A God Object is the limit of ignoring SRP: every actor edits it, every change risks everything, tests become impossible to isolate. SRP is the deliberate force that resists that drift.
- vs. Facade as the recombiner. After splitting, a thin
Facade(e.g.EmployeeService) can offer callers a single entry point over the three classes. Gain: SRP-clean internals with a simple external surface. Cost: one extra indirection layer. The Facade itself has one reason to change — the orchestration sequence — which keeps it SRP-clean too.
One-liner: Choose SRP when more than one actor edits the same module; prefer a single cohesive class when one owner controls all of it and splitting would only scatter tightly-related logic.
Takeaways
- SRP = one reason to change = one actor. Not “does one thing,” and not about feature count — it's about who can demand a change.
- The danger SRP prevents is accidental coupling: a shared helper or field that lets a change requested by one stakeholder silently break another's behavior.
- The fix is to split by stakeholder and share only immutable data; SRP deliberately tolerates duplication between code that changes for different reasons.
- Identify violations by the “and” test, by god-words like
Manager, and by tracking which teams' PRs touch the file — not by counting methods.
L0 · SRP = one reason to change = one actor (a single stakeholder) — not “one thing per class.”
L1 · ⑥ Cost/Simplicity — “so SRP just means keep classes small, one job each, right?”
Trap: “Yes — split until every class has one method / one verb.”
Bar: SRP counts actors, not verbs or method count; splitting-by-verb with no second actor buys zero coupling reduction and pays full navigation and DI-wiring cost. The deciding variable is how many distinct teams file change tickets against the file, not how many things it does. connects-to: SRP vs. coupling & cohesion
L2 · ⑤ Adversary/Edge — “you split Employee into three classes, but two still call a shared private helper — did that actually fix anything?”
Trap: “It's fine, that's just DRY reuse of a shared helper.”
Bar: A helper called by two actors' code paths is the violation, not a fix — a change filed by actor A silently mutates behavior actor B depends on, exactly like regularHours() leaking into reportHours(). The deciding variable is whether the shared code sits on the causal path of two independent change requests; if so it must be duplicated, or reduced to pure immutable data with zero editable logic. connects-to: coupling and SRP
L3 · ④ Time/Lifecycle — “six months later, one 'simple' feature now edits 12 files that used to be one class — did SRP fail?”
Trap: “Yes, over-engineered — merge them back for velocity.”
Bar: Twelve files touched by one feature is shotgun surgery, the signature of splitting along the wrong axis (verb or layer) instead of actor. Count how many of those 12 files are edited by the same actor for the same reason — if it's one actor they belong in one class or behind a facade; if each file has a distinct owner, the split was correct and the fragmentation is cohesion loss, not coupling. connects-to: cohesion and SRP
L4 · ⑤ Adversary/Edge + ⑥ Cost — “two teams each want a small tweak to the same validation logic — split preemptively now?”
Trap: “Always split early; SRP means separate every possible actor up front.”
Bar: A guessed boundary is more expensive than a temporarily fat class, because the wrong split must be undone later at real migration cost. The deciding variable is whether the second actor is a real, current, divergent change request today, not a hypothetical future owner — split on evidence of change history, not speculation. connects-to: Open/Closed — extension without the guess
L5 · ①Concurrency/②Failure + ⑥Cost — “PayCalculator and HourReporter now live in separately-deployed services; Finance ships a REGULAR_CAP change first — what breaks mid-rollout?”
Trap: “Nothing — they're separate classes/services now, fully decoupled.”
Bar: SRP decouples the edit surface, not the runtime's temporal consistency — during a partial rollout the two services can hold different values of the “coincidentally equal” constant simultaneously, producing real pay/report disagreement in production. The deciding variable is whether that tolerated duplication is contracted (versioned config, compatibility window) so independent deploys can't silently diverge — SRP's own trade-off (deliberate duplication) is the thing that now needs its own failure plan. connects-to: SOLID as one coherent trade-off system
The floor keeps dropping: now they ask what happens when a THIRD actor (Legal, for audit retention) needs a field that both PayCalculator and EmployeeRepository touch — do you add a fourth class, or does EmployeeData itself now have two reasons to change (schema shape for DBAs, retention shape for Legal)? Staff+ candidates recognize that immutable data objects aren't exempt from SRP forever — they just decouple later than behavior does.
Self-locate: died at L1 → mid-level; L4+ → staff signal.
Facing any new concept? Hit it with the six: concurrent? failing? at 100×? over time? adversarial? worth the cost? — that's the interviewer's whole playbook.
Sources: Robert C. Martin, “Clean Architecture” (2017), Ch. 7 — the canonical “one reason to change / one actor” formulation and the Employee example traced above; Robert C. Martin, “Agile Software Development: Principles, Patterns, and Practices” (2002); Martin's blog post “The Single Responsibility Principle” (2014), which corrects the common “one job” misreading. Re-authored and deepened for this guide: replaced the imprecise “one job” gloss and the army-knife-vs-knife analogy (which conflated multifunction-ness with reasons-to-change) with the actor-based definition, a numbered worked trace, compiling Java, a mechanism diagram, pitfalls, and selection trade-offs.
🤖 Don't fully get this? Learn it with Claude
Stuck on Introduction to the Single Responsibility Principle? Open Claude, copy a block below, and it'll teach you this exact concept — visually and interactively.
Build the mental picture, not memorization.
I just read a lesson on **Introduction to the Single Responsibility Principle** (OO & Low-Level Design) and want to truly understand it. Explain Introduction to the Single Responsibility Principle from first principles using ONE vivid real-world analogy and a visual mental model — draw it as ASCII art or a clear step-by-step diagram — with a concrete example using real numbers. Then ask me one question to check I got the mental picture, and wait for my reply. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
Socratic — adapts to where you're stuck.
Teach me **Introduction to the Single Responsibility Principle** interactively. Ask me ONE guiding question at a time, wait for my answer, and adapt to my confusion — build the idea with me step by step instead of explaining it all at once. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
Active recall exposes what you missed.
Quiz me on **Introduction to the Single Responsibility Principle** with 5 questions, easy to tricky, ONE at a time. Tell me if each answer is right; at the end, explain clearly what I got wrong and why. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
Intuition + hook + flashcards for long-term memory.
Help me remember **Introduction to the Single Responsibility Principle** for the long term: give the one-sentence intuition, a memorable hook/mnemonic, a tiny worked example, and 3 active-recall flashcards (Q -> A). If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.