Skip to main content

Chapter 7: SRP — The Single Responsibility Principle

"A module should be responsible to one, and only one, actor."

🎓 For New Learners

The Most Misunderstood Principle

SRP is almost universally misquoted as: "A class should do only one thing."

That is not what it means. A class can do many things and satisfy SRP. The real definition:

A module should have one, and only one, reason to change.

And "reason to change" means: one actor (a person or group of people) who can require that change. When a single module serves multiple actors, those actors' competing needs will cause unintended collisions.

The Two Symptoms

Symptom 1: Accidental Duplication

Imagine an Employee class with three methods:

  • calculatePay() — used by the Finance team
  • reportHours() — used by the HR team
  • save() — used by the DBA team

All three share a private helper regularHours(). Finance asks for a change to how regular hours are calculated. A developer updates regularHours() — unaware that reportHours() calls it too. Now HR reports are wrong. The actors' shared dependency caused a silent bug.

Symptom 2: Merges

Two developers modify Employee simultaneously — one changing calculatePay() for Finance, one changing reportHours() for HR. Git merge conflict. Risky merge. Potential bugs. The class serving multiple actors becomes a collision point.

The Solution: Separate the Actors

Move the data into a plain EmployeeData class with no methods. Give each actor its own class:

EmployeeData (plain data, no methods)

PayCalculator → used by Finance
HourReporter → used by HR
EmployeeRepository → used by DBAs

Now each class has exactly one actor. Changes requested by Finance cannot accidentally break HR. Merge conflicts disappear.

The Facade pattern can provide a single entry point if convenient:

public class EmployeeFacade {
private PayCalculator payCalc;
private HourReporter hourReporter;
private EmployeeRepository repo;

public Money calculatePay(Employee e) { return payCalc.calculate(e); }
public Hours reportHours(Employee e) { return hourReporter.report(e); }
public void save(Employee e) { repo.save(e); }
}

🔬 Senior Deep Dive

Actor, Not Function

The word "actor" is deliberate. An actor is a human role — a stakeholder group that has authority over and interest in a particular behavior. Finance owns pay calculation. HR owns hour reporting. DBAs own persistence schema.

When an actor requests a change, only their module changes. Other actors' modules are untouched. This is what makes the system safe to evolve.

Contrast with the common misreading "one function": a PayCalculator might have many methods — calculateRegularPay(), calculateOvertimePay(), calculateBonus(). All are owned by Finance. SRP is satisfied.

SRP at Multiple Scales

SRP applies at every level of abstraction:

LevelSRP Means
FunctionDoes one transformation; one reason to change
ClassServes one actor; one cohesive responsibility
ComponentDeployed for one team; one change cycle
ServiceOwned by one team; bounded context

Microservice boundaries are often SRP boundaries at the deployment level.

Common Spring Violations

A classic Spring SRP violation is the God Service:

@Service // serves: Finance, HR, DBAs, Audit team, Email team
public class EmployeeService {
public Money calculatePay(Long id) { ... } // Finance
public Hours reportHours(Long id) { ... } // HR
public void save(Employee e) { ... } // DBAs
public AuditLog getAuditLog(Long id) { ... } // Audit
public void sendWelcomeEmail(Employee e) { ... } // Email/Marketing
}

Five actors. Any change for any actor forces a retest of all other actors' behavior. Every release of this class carries risk for all five teams.

Refactored:

@Service public class PayrollService { ... } // Finance only
@Service public class HRReportingService { ... } // HR only
@Service public class EmployeeRepository { ... } // DBAs only
@Service public class AuditService { ... } // Audit only
@Service public class EmployeeOnboardingService { ... } // Email/Marketing

Detecting SRP Violations

Heuristics to spot violations in code review:

  • A class that requires changes from multiple teams per quarter
  • A class with imports from more than 2-3 unrelated domains
  • Methods in a class that share no common data
  • A class that is frequently the source of merge conflicts
  • A class that needs full regression testing whenever any part changes

Summary

ConceptKey Point
Real SRPOne reason to change = one actor
Accidental duplicationShared helpers serving multiple actors cause silent bugs
Merge collisionsMultiple actors → multiple developers → constant conflicts
SolutionSeparate classes per actor; use Facade for convenience
ScaleApplies from function to microservice boundary