Free lesson · Low Level Design Fundamentals
What Single Responsibility Actually Means
What "Single Responsibility" Actually Means
The Single Responsibility Principle is the first and most cited of the SOLID principles. The classical phrasing - "a class should have one reason to change" - sounds simple, but its application has produced more design arguments than any other OO principle. What counts as "one reason"? What's the unit of "responsibility"? When are two methods part of the same responsibility, and when are they two distinct ones?
The principle's vagueness is part of why it gets misapplied so often. Some interpret it as "one class, one method" and produce hundreds of micro-classes that fragment domain logic. Others interpret it as "do one thing" and end up with arbitrarily-bounded utility classes. Robert Martin (who popularized SOLID) eventually reframed the principle around actors and stakeholders - the people who request changes - which clarifies the original intent but doesn't eliminate the judgment calls.
In interviews, SRP is the principle most likely to be cited explicitly. The interviewer says "doesn't this violate SRP?" and the candidate is expected to engage with the substance, not just nod. Knowing the principle's nuances, common misinterpretations, and the actor-based framing distinguishes thoughtful candidates from those who recite the textbook line.
The Two Definitions
Robert Martin has given the principle two different formulations over the years. Both are widely cited; both are useful in different contexts.
The original, simpler form appeared in Agile Software Development: Principles, Patterns, and Practices (2002):
A class should have only one reason to change.
The refined form appeared in Clean Architecture (2017):
A module should be responsible to one, and only one, actor.
The two phrasings point at the same idea but emphasize different aspects. "One reason to change" frames the principle around what causes modifications: if two unrelated requirements would each prompt edits to the same class, the class has too many responsibilities. "Responsible to one actor" frames it around whose requirements the class serves: if marketing, finance, and operations all make demands on the same class, the class is serving multiple stakeholders and the inevitable conflicts will fragment it over time.
The actor framing is especially useful in larger organizations. Different departments have different change cadences, different priorities, different definitions of "correct." A class shared across them becomes a battleground; an SRP-respecting design gives each actor their own classes that can evolve at their own pace.
A Concrete Example
The textbook violation looks like this:
public class Employee {
private String name;
private BigDecimal salary;
private String department;
// Responsibility 1: Payroll calculations
public BigDecimal calculatePay() {
// Tax rules, deductions, bonuses, overtime
return salary.multiply(/* complex tax calculation */);
}
// Responsibility 2: Persistence
public void save(Connection conn) {
// SQL INSERT/UPDATE logic
}
// Responsibility 3: Reporting
public String reportHours() {
// Format hours worked for the HR weekly report
}
}
This Employee class has three responsibilities, each owned by a different actor in a typical company:
calculatePay()is owned by the finance team. Tax rules change when legislation changes. Bonus formulas change when compensation policy changes.save()is owned by the engineering/infrastructure team. The persistence mechanism changes when the database changes, when caching is added, when the schema evolves.reportHours()is owned by the HR team. The report format changes when HR decides to track different metrics, present them differently, or comply with new regulations.
Three different teams, three different change drivers, all converging on one class. When finance asks for a tax rule change, the engineering team must be careful not to break HR's reporting. When HR asks to add a new field to the report, the finance team's tax calculation might pull in stale data. Every change risks breaking the others.
The SRP-respecting decomposition:
public class Employee {
private final String id;
private final String name;
private final BigDecimal salary;
private final String department;
// Getters only - pure data carrier (or use a record)
}
public class PayrollCalculator {
public BigDecimal calculatePay(Employee employee) {
// Finance team owns this
}
}
public class EmployeeRepository {
public void save(Employee employee, Connection conn) {
// Engineering team owns this
}
}
public class HoursReportFormatter {
public String format(Employee employee, HoursLog hours) {
// HR team owns this
}
}
Each class now serves exactly one actor. Each can change at its own rate without disturbing the others. Bugs in payroll don't risk breaking persistence; HR's reporting changes don't risk breaking tax calculations.
The Tension With Information Expert
The astute reader may object: didn't Topic 4.4 argue for the Information Expert pattern, which says behavior should live where the data lives? An Employee should contain the operations that use its data, not push them out to external classes.
This tension is real. Two principles point in different directions:
- Information Expert says: put behavior with data. The Employee should compute its own pay.
- SRP says: don't combine behaviors with different change drivers. The Employee shouldn't both hold data and compute pay.
The resolution: both principles are correct, and they apply at different scales. Information Expert says if a piece of behavior fits the class's responsibility, it should live with the data. SRP says if a piece of behavior introduces a new responsibility, it should be a separate class.
For the Employee example: the simple accessors and immediate operations (like "is this employee in this department?") naturally live on Employee because they're part of being an Employee. Tax calculations, persistence, and HR reporting don't live on Employee because each introduces a separate responsibility with a separate change driver.
The judgment call: is this behavior part of what makes the class itself the class? Or is it a separate concern that happens to use the class's data? If the former, keep it; if the latter, split it.
What Counts as "One Reason"
The hardest part of applying SRP is judging what counts as one reason vs many. Consider:
public class Customer {
public boolean isValid() { /* ... */ }
public String formatName() { /* ... */ }
public Money calculateLifetimeValue() { /* ... */ }
}
Are these three responsibilities or one? Arguments both ways:
- Three: Validation, formatting, and value calculation are different concerns. They change for different reasons.
- One: All three operate on Customer data; all express what a Customer "knows about itself." Splitting them would over-fragment the class.
The right answer depends on context. In a small system, one Customer class with all three is reasonable - splitting them adds files and indirection without clear benefit. In a large system where the lifetime value calculation depends on complex external data, validation must satisfy multiple regulatory requirements, and formatting has many locale-specific variations, the splits are justified.
The actor framing helps: who would request a change to each method?
isValid()changes when business rules about valid customers change. Owned by product or compliance.formatName()changes when display requirements change. Owned by UX or product.calculateLifetimeValue()changes when business logic for valuation changes. Owned by analytics or marketing.
If those are three different teams with three different change cadences, SRP says split. If they're all owned by "the product team" and rarely change anyway, keeping them together is fine.
Common Misinterpretations
Warning: SRP is one of the most misinterpreted principles. The common failures:
"One method per class." Some take "one responsibility" to mean "one function." This produces hundreds of micro-classes (
AddCustomer,FindCustomer,DeleteCustomer) that fragment domain logic and produce coordination overhead. SRP says one responsibility, which usually contains several related methods."Don't combine concrete and abstract." Some apply SRP to insist every class be either pure abstraction or pure implementation. This produces unnecessary interfaces and ceremony. SRP is about responsibility (what changes together), not about the abstract/concrete divide.
"Split classes by type of method." Some split classes by whether methods are "queries" vs "commands," or "domain" vs "infrastructure." This may produce useful splits but it isn't what SRP says. The split should be by what changes together, not by the kind of method.
"Put each public method in its own class." Closely related to the first. Produces command-object explosions. SRP doesn't require this.
"Apply SRP everywhere immediately." Junior teams often discover SRP and start splitting every class. The result is a fragmented codebase with too many tiny pieces. SRP is a guideline, not an absolute - apply it when a class is causing real problems, not preemptively.
When SRP Becomes Obvious - The Pain Indicators
Classes that violate SRP cause specific problems. Spotting these problems is often what triggers the SRP refactor:
Frequent conflicts in code review. Two engineers working on different features both modify the same class. The merge is painful because the changes are unrelated and conflict in non-obvious ways.
Tests that overlap awkwardly. A test that's nominally about "Employee" sets up payroll data, mocks the database, and verifies HR formatting - because the Employee class does all of those things. The tests are slow, fragile, and hard to understand.
Sprawling import lists. A class importing both java.sql.Connection and java.util.Locale and org.springframework.security.Principal is touching too many concerns.
Modifying X to support a change in Y. A bug in HR reporting requires editing the Employee class even though Employee's other responsibilities are stable. The change risks breaking unrelated functionality.
The "I just need to add a field" problem. Adding a field for one responsibility breaks tests for another responsibility because the same class is involved in both.
When these patterns appear, SRP is being violated and the refactor (covered in Topic 8.4) is the fix.
The Difference From Cohesion
SRP is sometimes equated with cohesion - the property that methods within a class belong together. They're related but distinct:
- Cohesion is about whether methods inside a class share purpose. A class with high cohesion has methods that do related things.
- SRP is about whether the class's purpose is singular. A class with single responsibility has one reason to be modified.
A class can be cohesive but violate SRP if the cohesion comes from "all methods operate on the same data" rather than "all methods serve the same responsibility." A class can be SRP-respecting but have weak cohesion if the methods are all about one responsibility but don't share data.
In practice, the two correlate strongly. Classes with single responsibility tend to be cohesive; classes with multiple responsibilities tend to have weaker cohesion. But the principles are different lenses on the same design space.
SRP at Different Scales
SRP applies at every scale, not just classes. The principle is stated for "modules" or "classes" but the same logic works for methods, packages, and entire services:
Method level. A method that does multiple things violates SRP at its scale. The fix is usually to extract sub-methods.
Class level. The classical formulation. Covered in this module.
Package level. A package containing classes serving multiple actors violates SRP. The fix is to split into focused packages.
Module level. A library or Maven module serving multiple change drivers violates SRP. The fix is to split into focused modules.
Service level. A microservice serving multiple actors with different change cadences violates SRP. The fix is to split into focused services.
The same principle - one reason to change, one actor served - applies at each scale. The right scale for any specific application of SRP depends on the size of the system and the complexity of the responsibilities involved.
Why This Matters in Interviews
Interview: SRP is the most-named SOLID principle in interviews. Direct probes you should expect:
- "What's the Single Responsibility Principle?" - Testing whether you can state both formulations and their motivation.
- "Does this class violate SRP?" - Testing whether you can apply the principle to specific code.
- "How would you refactor this?" - Testing the refactoring path (covered in Topic 8.4).
- "What's the actor for this class?" - Testing the modern actor-based framing.
Implicit evaluation: When you design classes during a problem, the interviewer watches for whether each class has a clear single responsibility. Classes that do "everything for X" generate negative signal. Classes named for specific responsibilities (Calculator, Validator, Formatter, Repository) and bounded accordingly generate positive signal.
Common follow-up probes:
- "What if we needed to change X?" - Testing whether your design isolates change.
- "What if two teams owned different parts of this?" - Testing the actor framing.
- "When would you NOT apply SRP?" - Testing whether you understand the principle's limits.
A strong articulation during design: "I'll keep Employee as a data class and put payroll logic in a PayrollCalculator. They have different reasons to change - Employee changes when our data model evolves, PayrollCalculator changes when tax rules change. Different actors, different cadences."
Common Mistakes
Warning: Beyond the misinterpretations above, recurring SRP failures:
God classes that "manage" everything.
OrderManager,UserService,DataManager- names that signal a single class is responsible for many things. The name itself indicates the SRP violation.Classes that mix abstraction levels. A class doing both high-level orchestration ("complete the order") and low-level details ("insert this row into the database") is serving two responsibilities at different abstraction layers.
Classes that mix concerns. Domain logic mixed with infrastructure. Business rules mixed with formatting. Validation mixed with persistence. Each combination is two responsibilities forced together.
Reactive over-splitting. Discovering SRP and immediately splitting every class. The codebase fragments; coordination overhead explodes. SRP is a tool for solving specific problems, not a universal command.
Splitting along the wrong axis. Splitting Employee into
EmployeeDataandEmployeeBehaviorseparates data from behavior, but doesn't address the multiple-responsibility problem. The split should be by responsibility, not by structure.
Try It
Exercise: For each class, identify the responsibilities and the actors. Decide whether SRP is violated and propose how to split if so:
Userwith methods:validateEmail(),hashPassword(),save(Connection),sendWelcomeEmail(),calculateAge()
Invoicewith methods:total(),addLineItem(),printToPdf(),sendByEmail(String address),markAsPaid()
Calculatorwith methods:add(int, int),subtract(int, int),multiply(int, int),divide(int, int),formatResult(int)
OrderProcessorwith methods:validateOrder(Order),chargePayment(Order),notifyWarehouse(Order),sendConfirmationEmail(Order),logToAuditTrail(Order)
Documentwith methods:getContent(),setContent(String),getWordCount(),printToPrinter(),saveToDisk(),convertToHtml()For each, list:
- The responsibilities you identify
- The likely actor for each
- The proposed class decomposition
- One responsibility that might reasonably stay on the original class
Then audit a class you've worked on. What are its responsibilities? Who are the actors? Has the class been a source of merge conflicts or test fragility? If yes, that's evidence that SRP is being violated.