Single Responsibility Principle (SRP)

6 min read

Reading Progress0%
Object-Oriented Design Index
Tier 1 -- Foundations
Tier 2
Tier 3
Tier 4
Tier 5
Object-Oriented Design Index
Tier 1 -- Foundations
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.


QUICK CHECK

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?

Choose one answer

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 this
  • report_hours() — COO cares about this
  • save() — 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");
    }
}

QUICK CHECK

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?

Choose one answer

3. Variants & Comparisons

SRP is interpreted at different granularities — class, module/package, microservice. The principle scales.

ApproachHow It WorksProsConsBest For
Actor-based splitSeparate class per stakeholder groupPrecise, Martin's intentCan feel over-engineered for simple CRUDBusiness-domain classes with multiple owners
Concern-based splitSeparate class per concern (data, logic, persistence)Clean layeringBlurs who "owns" each layerLayer-structured architectures (MVC, repository pattern)
Method-count heuristicSplit when a class exceeds ~5–7 public methodsSimple rule of thumbMechanical, ignores actual change driversQuick code reviews
Package-level SRPGroup classes that change together into one packageManages large codebasesRequires explicit ownership conventionsModular monoliths or microservices

QUICK CHECK

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?

Choose one answer

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 sqlalchemy all 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 EmployeeDataFormatter and EmployeeStringFormatter as 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_, and save_ methods all in one place — reach for SRP."
  • "If a bug in payroll is found by running HR report tests — SRP is violated."

QUICK CHECK

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?

Choose one answer

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.


QUICK CHECK

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?

Choose one answer

6. Interview Cheat Sheet

Key sentences to say:

  1. "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.'"
  2. "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."
  3. "The fix isn't always splitting into smaller methods — it's splitting into separate classes so each actor's code evolves independently."
  4. "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.