Why this matters
SRP is commonly misread as "a module should do only one thing." The real definition, as Robert Martin clarifies in Clean Architecture, is more precise: a module should be responsible to one, and only one, actor. An actor is a group of people — a department, a role — whose requirements drive changes to that module.
The canonical example is an Employee class with three methods: calculatePay() requested by the CFO, reportHours() requested by the COO, and save() requested by the CTO. When these three methods share a private helper — say, _regularHours() — the CFO's team can accidentally break the COO's reports simply by adjusting the pay calculation algorithm. The two actors are now unknowingly coupled through a shared class. Cohesion is the force that keeps things that change for the same reason together; SRP is the principle that separates things that change for different reasons.
The problem
Three actors — CFO, COO, CTO — all depend on one class. A change requested by one actor silently breaks the logic used by another.
Bad
class Employee:
def calculate_pay(self) -> float: # owned by CFO
hours = self._regular_hours()
return hours * self.hourly_rate
def report_hours(self) -> float: # owned by COO
hours = self._regular_hours() # reuses same helper — coupling!
return hours
def save(self) -> None: # owned by CTO / DBA
db.save(self)
def _regular_hours(self) -> float: # shared — dangerous
return self.total_hours - self.overtime_hours
class Employee {
calculatePay(): number { // CFO's concern
return this.regularHours() * this.hourlyRate;
}
reportHours(): number { // COO's concern
return this.regularHours(); // accidental coupling!
}
save(): void { // CTO's concern
db.save(this);
}
private regularHours(): number { // shared — fragile
return this.totalHours - this.overtimeHours;
}
}
The solution
Three separate classes, one actor each. The Employee data structure holds state only — it has no behavior. Each class owns its own helper logic.
Good
class Employee:
"""Data structure only — no behavior."""
total_hours: float
overtime_hours: float
hourly_rate: float
class PayCalculator: # answers to CFO
def calculate_pay(self, emp: Employee) -> float:
return self._regular_hours(emp) * emp.hourly_rate
def _regular_hours(self, emp: Employee) -> float:
return emp.total_hours - emp.overtime_hours
class HourReporter: # answers to COO
def report_hours(self, emp: Employee) -> float:
return emp.total_hours # uses its own definition
class EmployeeRepository: # answers to CTO / DBA
def save(self, emp: Employee) -> None:
db.save(emp)
interface Employee {
totalHours: number;
overtimeHours: number;
hourlyRate: number;
}
class PayCalculator { // answers to CFO
calculatePay(emp: Employee): number {
return this.regularHours(emp) * emp.hourlyRate;
}
private regularHours(emp: Employee): number {
return emp.totalHours - emp.overtimeHours;
}
}
class HourReporter { // answers to COO
reportHours(emp: Employee): number {
return emp.totalHours; // owns its own definition
}
}
class EmployeeRepository { // answers to CTO
save(emp: Employee): void {
db.save(emp);
}
}
Key takeaway
SRP is not about doing one thing — it's about serving one actor. If two different executives can request conflicting changes to the same module, that module is violating SRP and is a ticking time bomb for accidental coupling.