Single Responsibility Principle
One reason to change
The Single Responsibility Principle (SRP), introduced by Robert Martin, states that each class should serve one actor and have one reason to change. A class has a single responsibility when only one kind of change in the system would require editing it. A useful diagnostic is to ask "what would make me open this file?" If the answer involves more than one department — the finance team changes invoice fields, the ops team changes email formatting, and the DBA changes the persistence query — the class is handling more than one concern.
The principle is not about making classes small for its own sake. A class can have many methods and still have a single responsibility. The problem arises when a class answers to multiple stakeholders whose requirements pull in different directions.
What the violation looks like in practice
A common place this accumulates is the central entity class early in a project. Consider an
Invoice class that starts life holding line items and a total. As the product grows, developers
attach email formatting logic ("we need to send this") and database persistence ("we need to save
this") directly to the same class. The result is an object that knows about SMTP servers and SQL
tables in addition to business arithmetic.
1# Bad: Invoice knows about business rules, email formatting, AND database writes.
2# Three different teams have reasons to edit this single class.
3import smtplib
4import sqlite3
5
6class Invoice:
7 def __init__(self, customer: str, items: list[tuple[str, float]]):
8 self.customer = customer
9 self.items = items
10
11 def total(self) -> float:
12 return sum(price for _, price in self.items)
13
14 # Email concern — owned by the notifications team
15 def send_email(self, to_address: str) -> None:
16 body = f"Dear {self.customer}, your total is ${self.total():.2f}"
17 with smtplib.SMTP("smtp.example.com") as server:
18 server.sendmail("billing@example.com", to_address, body)
19
20 # Persistence concern — owned by the data team
21 def save(self, db_path: str) -> None:
22 conn = sqlite3.connect(db_path)
23 conn.execute(
24 "INSERT INTO invoices (customer, total) VALUES (?, ?)",
25 (self.customer, self.total()),
26 )
27 conn.commit()
28 conn.close()