One reason to change
SRP is not 'do one thing'. A module should have one reason to change — be responsible to one actor whose changing requirements force edits. Merging actors (Finance + HR + DBA) into one class is the violation; split by actor.
Finance asks for a change to how overtime pay is calculated. You edit Employee.calculatePay(), ship it, and a week later HR files a bug: the hours report on their dashboard is now subtly wrong. You never touched the report. But the two methods shared a helper, and your “pay” change leaked into “hours”. Two departments, one class, one accident waiting to happen.
This is the failure the Single Responsibility Principle exists to prevent — and almost everyone first learns SRP as the wrong rule. “A class should do one thing” sounds right and is nearly useless: it’s vague enough to justify any split and any merge. The real principle is sharper, and it’s about who can make you change the code.
After this lesson you can state SRP in Uncle Bob’s precise framing — a module should have exactly one reason to change, where a “reason” is an actor whose requirements drive edits; identify the actors behind a class’s methods and recognise when several actors are coupled into one module; split a class by actor instead of by verb; and avoid the opposite failure of shredding a class into one-method fragments by misreading SRP as “one public method per class”.
SRP is about one reason to change, not one thing done. Robert Martin’s own correction is blunt: the principle was never “a module should do one thing.” That folk version is what most people remember, and it’s why SRP gets argued about endlessly — “does parsing and validating count as one thing or two?” has no answer. The actual statement is: a module should have one, and only one, reason to change. A “reason to change” is not a feature or a method; it is a source of requirements — a person or role who can walk in and demand the behaviour be different.
// "Does this do one thing?" — unanswerable, and the wrong question.
// "Who can ask me to change this?" — answerable, and the right one.The shift from counting verbs to identifying requesters is the whole lesson.
A “reason to change” is an actor. Martin sharpens “reason to change” into actor: the group of users or stakeholders for whom a given change is made. Finance defines how pay is computed. HR defines what counts as reportable hours. The DBA team defines the persistence schema. Each is a separate actor with separate requirements that drift on their own schedule. The canonical violation is one class serving all three:
class Employee {
calculatePay(): Money { /* Finance owns this rule */ }
reportHours(): Hours { /* HR owns this rule */ }
save(): void { /* DBA owns this schema */ }
}Three actors. Three reasons to change. SRP says: a module answerable to three actors is three responsibilities wearing one name.
The danger isn’t ugliness — it’s accidental coupling between actors. The reason this is a principle and not a style note is the failure mode it prevents. When two actors share a class, they tend to share private helpers, and a change requested by one actor silently alters behaviour the other actor depends on.
class Employee {
// Both pay and the HR report call this. Finance "owns" it conceptually.
private regularHours(): Hours { /* shared helper */ }
calculatePay(): Money { return rate.times(this.regularHours()); } // Finance
reportHours(): Hours { return this.regularHours(); } // HR reads the SAME helper
}Finance asks to change how regularHours() rounds, for payroll reasons. You make the edit. HR’s report — which had no reason to change — now reports different numbers. Nobody asked for that. That is the catastrophe SRP guards against: a Finance requirement reaching out and breaking an HR feature because they were welded together. The coupling is invisible until it ships a wrong report.
Split by actor: one module per source of requirements. The fix is not “make each method its own class.” It is to separate the data from the actor-specific behaviours, so each policy lives with the actor who owns it and changes only when that actor changes.
class EmployeeData { /* just the fields; no business rules */ }
class PayCalculator { calculatePay(e: EmployeeData): Money { /* Finance only */ } }
class HoursReporter { reportHours(e: EmployeeData): Hours { /* HR only */ } }
class EmployeeRepository { save(e: EmployeeData): void { /* DBA only */ } }Now a Finance change touches PayCalculator and stops there. If pay and reporting genuinely need the same “regular hours” computation, that shared logic must be moved deliberately into its own owned place — not left as a private helper two actors happen to call. The test for “is this one responsibility?” is: could two different actors request conflicting changes to it? If yes, it’s at least two responsibilities, regardless of how few methods it has.
The coupling fires — then the split contains it. Before: one class, three actors, a shared private helper.
class Employee {
constructor(private timecards: Timecard[], private rate: Money) {}
// shared by pay AND the HR report
private regularHours(): number {
// round each shift DOWN to the quarter hour
return this.timecards.reduce((h, t) => h + Math.floor(t.hours * 4) / 4, 0);
}
calculatePay(): Money { return this.rate.times(this.regularHours()); } // Finance
reportHours(): number { return this.regularHours(); } // HR
}Finance asks: “Stop rounding pay down — round to the nearest quarter, payroll was under-paying.” You change Math.floor to Math.round inside regularHours(). Payroll is fixed. But reportHours() calls the same helper, so HR’s compliance report — which legally must round down — now rounds to nearest. One Finance edit, one broken HR feature, zero HR requests. That’s actor coupling detonating.
After: split by actor, and the shared logic is owned explicitly rather than shared by accident.
class EmployeeData {
constructor(readonly timecards: Timecard[], readonly rate: Money) {}
}
// Finance owns its own rounding rule
class PayCalculator {
pay(e: EmployeeData): Money {
const hours = e.timecards.reduce((h, t) => h + Math.round(t.hours * 4) / 4, 0);
return e.rate.times(hours);
}
}
// HR owns its own rounding rule — and it can never be changed by a Finance edit
class HoursReporter {
report(e: EmployeeData): number {
return e.timecards.reduce((h, t) => h + Math.floor(t.hours * 4) / 4, 0);
}
}Now Finance’s “round to nearest” is a one-line change inside PayCalculator, and HoursReporter is physically incapable of being affected by it. The two actors’ rules diverged — which is exactly what they were always allowed to do. SRP didn’t add classes for neatness; it gave each actor a wall.
▸Why this works
Why phrase the rule as “reason to change” instead of just “group related methods”? Because cohesion-by-topic and cohesion-by-actor disagree exactly where it matters. calculatePay, reportHours, and save are all “about an employee” — topically they belong together, which is why the bad design feels natural. SRP overrides topical grouping with change grouping: keep together what changes together for the same reason, and separate what changes for different reasons even if it’s about the same noun. The unit of cohesion is the actor, not the entity. That’s the senior reframe — you stop drawing class boundaries around data and start drawing them around who can demand a change.
▸Common mistake
The opposite over-correction is just as wrong: reading SRP as “one public method per class” and shredding everything into PayCalculatorForFullTimeEmployees, PayCalculatorForContractors, RegularHoursRounder, OvertimeRounder… This is anemic fragmentation: dozens of one-method classes that all change together whenever Finance changes, now spread across ten files with the logic smeared between them. You’ve multiplied the surface area without reducing the number of reasons to change — in fact you’ve made the single Finance responsibility harder to edit. SRP is satisfied when each module has one reason to change, not when each module has one method. If two pieces always change for the same actor, they belong in the same module; splitting them violates SRP just as surely as merging two actors does.
A class has only ONE public method, generateInvoice(), but inside it computes tax (Finance's rule), formats currency for the UI (a design/locale concern), and writes a row to the ledger table (the DBA's schema). Does it satisfy SRP?
SRP is not “do one thing” — it is one reason to change, where a reason is an actor: a source of requirements who can demand the code be different. The canonical violation is the Employee class owning calculatePay() (Finance), reportHours() (HR), and save() (DBA) — three actors welded into one module, so a change requested by one can break a feature the other relies on through a shared helper. The fix is to split by actor, giving each source of requirements its own module and its own wall, so a change’s blast radius stops at the actor who asked for it. Guard both edges: merging actors couples unrelated change, but shredding one actor’s logic into one-method classes is the same sin inverted — anemic fragmentation that multiplies files without reducing reasons to change. The unit of responsibility is the actor.
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.