bigonotes
CS ConceptsSOLID Principles

Single Responsibility Principle

Why a class should have only one reason to change, and how to split a do-everything class into focused ones.

2 min readLast updated SOLID Principles

The Single Responsibility Principle (SRP) says a class should have only one reason to change. Put differently, each class does one job well. When a class mixes business logic, storage, emailing and tax rules, a change to any of them risks breaking the others.

The idea#

Think of kitchen appliances: a toaster toasts, a blender blends, a microwave heats. One machine that tried to do all three would be complicated and break easily.

A quick test: describe the class in one sentence. If you need the word "and" ("it stores employee data and saves to the database and sends email"), it probably has more than one responsibility.

"Reason to change" usually maps to a person or team who would ask for the change: HR changes pay rules, the DBA changes storage, marketing changes email templates.

Violation#

This Employee class does four unrelated jobs:

class Employee:
    def __init__(self, name: str, salary: float):
        self._name = name
        self._salary = salary

    def work(self) -> None:              # employee behavior
        print(f"{self._name} is working")

    def save_to_database(self) -> None:  # persistence
        print("Saving to database...")

    def send_email(self) -> None:        # notifications
        print("Sending email...")

    def calculate_tax(self) -> None:     # tax rules
        tax = self._salary * 0.2
        print(f"Tax: {tax}")


Employee("Ana", 50_000).calculate_tax()  # Tax: 10000.0

Four responsibilities, so four different reasons this one file might change.

Fix#

Split each responsibility into its own class:

class Employee:
    def __init__(self, name: str, salary: float):
        self._name = name
        self._salary = salary

    def work(self) -> None:
        print(f"{self._name} is working")

    @property
    def salary(self) -> float:
        return self._salary


class EmployeeRepository:
    def save(self, emp: Employee) -> None:
        print("Saving employee to database...")


class EmailService:
    def send_email(self, to: str, message: str) -> None:
        print(f"Sending email to {to}")


class TaxCalculator:
    def calculate_tax(self, salary: float) -> float:
        return salary * 0.2


ana = Employee("Ana", 50_000)
EmployeeRepository().save(ana)
print(TaxCalculator().calculate_tax(ana.salary))  # 10000.0

Each class now has one clear job and can be changed and tested in isolation.

Trade-offs#

SRP can be overdone:

  • Splitting too aggressively creates many tiny classes that are hard to navigate. Group by reason to change, not by method count.
  • SRP doesn't forbid a class having several methods; they just need to serve the same purpose.

Key takeaways#

  • One class, one responsibility, one reason to change.
  • The "and" test catches most violations.
  • Move persistence, notification and calculation logic into their own classes.
  • Smaller, focused classes are easier to test and safer to modify.

Found a mistake? Tell me on LinkedIn ↗

Further reading#