6 min read
Object-Oriented Design Index
Tier 1 -- Foundations
Core Concepts
SOLID Principles
Creational Patterns
Structural Patterns
Tier 2
Tier 3
Tier 4
Tier 5
Object-Oriented Design Index
Tier 1 -- Foundations
Core Concepts
SOLID Principles
Creational Patterns
Structural Patterns
Tier 2
Tier 3
Tier 4
Tier 5
Single Responsibility Principle (SRP)
1. What Is It?
The states that each software module should have one and only one reason to change. Robert C. Martin later refined this in Clean Architecture as: "A module should be responsible to one, and only one, actor" — where an "actor" is a group of stakeholders who share the same motivation for requesting changes.
Without SRP, a class that serves multiple stakeholders becomes a liability. A change requested by one business function (e.g., payroll recalculating overtime) can inadvertently break another function (e.g., HR reporting hours) because both depend on shared logic inside the same class. SRP forces these concerns apart so each change is isolated to the class that owns it.
A single Employee class handles both payroll calculations (used by the Finance team) and HR hours reporting (used by the HR team). The Finance team requests a change to how overtime is calculated, and after the update, the HR hours report starts producing incorrect results. What does this scenario best illustrate?
2. How It Works
The core mechanic: identify the actors (stakeholders or business functions) whose requirements could cause a class to change. If you can name more than one, split the class.
Martin's canonical example is an Employee class with three methods:
calculate_pay()— CFO cares about thisreport_hours()— COO cares about thissave()— CTO (database team) cares about this
All three methods share private helper logic (e.g., regular_hours()). When the CFO requests a change to how overtime is computed, a developer modifying calculate_pay() may accidentally break report_hours() — a completely different actor's concern.
Mermaid Class Diagram
Python Example
from dataclasses import dataclass from abc import ABC, abstractmethod # BEFORE — SRP violation: one class serves multiple actors class Employee: def __init__(self, name: str, hours_worked: float, hourly_rate: float) -> None: self.name = name self.hours_worked = hours_worked self.hourly_rate = hourly_rate def calculate_pay(self) -> float: # CFO's concern return self._regular_hours() * self.hourly_rate def report_hours(self) -> float: # COO's concern — shares _regular_hours with calculate_pay! return self._regular_hours() def save(self) -> None: # CTO's concern print(f"Saving {self.name} to database") def _regular_hours(self) -> float: return min(self.hours_worked, 40.0) # AFTER — SRP applied: one class per actor @dataclass class EmployeeData: """Pure data container — no behavior, no actor.""" name: str hours_worked: float hourly_rate: float class PayCalculator: """Owned by the CFO. Changes only when payroll rules change.""" def calculate_pay(self, employee: EmployeeData) -> float: regular_hours = min(employee.hours_worked, 40.0) return regular_hours * employee.hourly_rate class HourReporter: """Owned by the COO. Changes only when reporting requirements change.""" def report_hours(self, employee: EmployeeData) -> float: return min(employee.hours_worked, 40.0) class EmployeeRepository: """Owned by the engineering/DBA team. Changes only when persistence changes.""" def save(self, employee: EmployeeData) -> None: print(f"Saving {employee.name} to database")
Java Example
// BEFORE — SRP violation public class Employee { private String name; private double hoursWorked; private double hourlyRate; public double calculatePay() { return regularHours() * hourlyRate; // CFO's concern } public double reportHours() { return regularHours(); // COO's concern — shares regularHours! } public void save() { System.out.println("Saving " + name); // CTO's concern } private double regularHours() { return Math.min(hoursWorked, 40.0); } } // AFTER — SRP applied // Pure data container public class EmployeeData { public final String name; public final double hoursWorked; public final double hourlyRate; public EmployeeData(String name, double hoursWorked, double hourlyRate) { this.name = name; this.hoursWorked = hoursWorked; this.hourlyRate = hourlyRate; } } // CFO's class public class PayCalculator { public double calculatePay(EmployeeData employee) { double regularHours = Math.min(employee.hoursWorked, 40.0); return regularHours * employee.hourlyRate; } } // COO's class public class HourReporter { public double reportHours(EmployeeData employee) { return Math.min(employee.hoursWorked, 40.0); } } // CTO's class public class EmployeeRepository { public void save(EmployeeData employee) { System.out.println("Saving " + employee.name + " to database"); } }
A UserService class has three methods: calculateDiscount() (used by the pricing team), generateActivityReport() (used by the analytics team), and save() (used by the database team). All three methods share a private helper getActiveMonths(). The pricing team requests a change to discount logic, so a developer modifies calculateDiscount() and inadvertently changes the shared getActiveMonths() helper. What is the most likely consequence?
3. Variants & Comparisons
SRP is interpreted at different granularities — class, module/package, microservice. The principle scales.
| Approach | How It Works | Pros | Cons | Best For |
|---|---|---|---|---|
| Actor-based split | Separate class per stakeholder group | Precise, Martin's intent | Can feel over-engineered for simple CRUD | Business-domain classes with multiple owners |
| Concern-based split | Separate class per concern (data, logic, persistence) | Clean layering | Blurs who "owns" each layer | Layer-structured architectures (MVC, repository pattern) |
| Method-count heuristic | Split when a class exceeds ~5–7 public methods | Simple rule of thumb | Mechanical, ignores actual change drivers | Quick code reviews |
| Package-level SRP | Group classes that change together into one package | Manages large codebases | Requires explicit ownership conventions | Modular monoliths or microservices |
A team is doing a quick code review and notices that a service class has grown to 12 public methods. They want a fast, low-effort way to decide whether to split it — without deeply analyzing change history or stakeholder ownership. Which SRP approach best fits this situation, and what is its key trade-off?
4. When to Use It (and When NOT To)
Use SRP when:
- A class has methods that different teams or business functions call and modify independently
- A change to one feature keeps breaking unrelated tests for a different feature in the same class
- A class has imports from multiple unrelated domains (e.g.,
import csv,import smtplib,import sqlalchemyall in one class)
Anti-patterns:
- God class: One class does everything — data access, business logic, formatting, logging. Every new feature touches it.
- Over-splitting: Creating a separate class for every method. SRP is about reasons to change, not lines of code. If two methods always change together, they belong together.
- Splitting on implementation, not : Creating
EmployeeDataFormatterandEmployeeStringFormatteras separate classes because they use different internal logic — but both serve the same UI actor.
Decision triggers:
- "If you see a class with
calculate_,report_,send_, andsave_methods all in one place — reach for SRP." - "If a bug in payroll is found by running HR report tests — SRP is violated."
A developer notices that a UserManager class has a calculate_discount() method, a send_welcome_email() method, a save_to_database() method, and a generate_report() method. A bug in the discount calculation is discovered only when the reporting tests fail. Which of the following best describes the problem and the appropriate response?
5. Real-World Usage
Django's class-based views: Django separates View (HTTP handling), Model (data/persistence), and Form (validation) — each owned by a different concern. Mixing model logic into views is a common anti-pattern Django's layered design resists.
Spring's @Repository, @Service, @Controller annotations: Spring enforces layer separation at the annotation level, making the actor-to-layer mapping explicit. A @Service should not contain SQL; a @Repository should not contain business rules.
Java's java.io package: InputStream handles raw bytes. InputStreamReader handles character encoding conversion. BufferedReader handles line-by-line reading. Each class has exactly one reason to change — the format it handles — following SRP at the class level.
A Spring developer places a SQL query directly inside a @Service class to fetch user records, reasoning that it keeps the feature in one place. Which Single Responsibility Principle concern does this violate?
6. Interview Cheat Sheet
Key sentences to say:
- "SRP says a class should have one reason to change — and that reason maps to a specific actor or stakeholder, not just a vague 'responsibility.'"
- "The classic violation is a class that the CFO, COO, and CTO all depend on — any one of them requesting a change risks breaking the others' features."
- "The fix isn't always splitting into smaller methods — it's splitting into separate classes so each actor's code evolves independently."
- "Over-applying SRP leads to over-engineering — two methods that always change together belong in the same class."
Common follow-up questions:
"How do you know when a class has too many responsibilities?"
Look at the import statements and the test file. If tests for feature A keep failing when you change feature B, the class serves two actors.
"Isn't SRP just common sense?"
The subtlety is in the word 'reason.' Two methods that both do formatting are not one responsibility if one formats for the UI team and one formats for the export team — those are two actors with different change drivers.
"How does SRP relate to ?"
SRP is the principle; high is the measurable outcome. A class that follows SRP will be highly cohesive — all its methods relate to the same actor's concerns.
Connections to other concepts:
- SRP enables Open/Closed Principle — when each class has one reason to change, extending behavior rarely requires modifying existing classes
- SRP is enforced by — when you inject dependencies, you're forced to think about which actor each class serves
- Violations of SRP are often the root cause of shotgun surgery (one logical change requires modifying many classes) and divergent change (one class changes for many different reasons)
Glossary History
Click dotted jargon to save explanations here.
Glossary History
Click dotted jargon to save explanations here.