Skip to main content
Clean Architecture 70 XP · 7 min

SRP: The Single Responsibility Principle

A module should be responsible to only one actor — preventing changes for one department from accidentally breaking another.

Showing
Ad (728×90)

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.

Done with this lesson?

Mark it complete to earn XP and track your progress.