What is the Single Responsibility Principle?
The Single Responsibility Principle (SRP) — the S in SOLID — states that a class should have only one reason to change. This means a class should do one thing and do it well. If a class handles multiple concerns (data, persistence, rendering, email sending), then changes to any of those concerns force changes to the class.
Explanation
Real-World Analogy
- A Swiss Army knife 🔧 is great for camping survival, but you wouldn’t use it as your primary tool in a professional kitchen. A chef uses a dedicated chef’s knife, a dedicated peeler, a dedicated bread knife — each tool does one thing extremely well. When the bread knife breaks, you only replace the bread knife, not the whole Swiss Army knife.
- In code: if a class handles 5 concerns, a change in any one of them risks breaking the other 4.
What “Reason to Change” Means
- A “reason to change” = a stakeholder or actor who can demand a change to that class:
class UserManager: ← Has 4 reasons to change!
def create_user() ← Business rules team
def save_to_database() ← DB team / DBA
def send_welcome_email() ← Marketing team
def generate_report() ← Reporting team
If marketing changes email format → we touch UserManager
If DBA changes schema → we touch UserManager
If business changes validation → we touch UserManager
If reporting changes format → we touch UserManager
Every change = risk of breaking the other 3 concerns!
The God Class Anti-Pattern
- SRP violations often appear as God Classes — classes that know and do too much:
| God Class Signs | SRP Solution |
|---|---|
| >500 lines | Split into multiple focused classes |
| Method names from different domains (save, email, render, validate) | Separate by domain/concern |
| Many different imports (DB, email, HTTP, file IO) | Separate by infrastructure concern |
| Hard to unit test without mocking 10 things | Each class = testable in isolation |
Before & After
# ❌ SRP Violation — UserManager does EVERYTHING
class UserManager:
def create_user(self, name: str, email: str) -> dict:
user = {"name": name, "email": email}
# Validation
if "@" not in email: raise ValueError("Bad email")
# Persistence
self._db.save("users", user)
# Email
self._email_service.send(email, "Welcome!", f"Hello {name}")
# Logging
self._logger.info(f"User created: {email}")
# Report
self._reporter.add_event("user_created", email)
return user
# 4 reasons to change: DB changes, email changes, logging changes, report changes
# ✅ SRP — Each class has ONE reason to change
class UserValidator:
"""One reason to change: business rules for user validity."""
def validate(self, name: str, email: str) -> None:
if not name.strip(): raise ValueError("Name required")
if "@" not in email: raise ValueError("Invalid email")
class UserRepository:
"""One reason to change: database schema or storage mechanism."""
def __init__(self, db): self.db = db
def save(self, user: dict) -> None: self.db.save("users", user)
def find_by_email(self, email: str) -> dict: return self.db.find("users", email=email)
class UserEmailService:
"""One reason to change: email templates or provider."""
def __init__(self, mailer): self.mailer = mailer
def send_welcome(self, name: str, email: str) -> None:
self.mailer.send(email, "Welcome!", f"Hello {name}!")
class UserService:
"""Orchestrates — one reason to change: user creation workflow."""
def __init__(self, validator, repo, emailer):
self.validator = validator
self.repo = repo
self.emailer = emailer
def create_user(self, name: str, email: str) -> dict:
self.validator.validate(name, email)
user = {"name": name, "email": email}
self.repo.save(user)
self.emailer.send_welcome(name, email)
return userImplementation
-
Applying SRP to a report generation system — splitting data retrieval, formatting, and export into separate classes. Python · Java · Java Script
Languages:
# ─── Python — SRP applied to Report Generation ───────────────────────
from dataclasses import dataclass
from datetime import date
import csv
import json
@dataclass
class SalesRecord:
product: str
quantity: int
price: float
sale_date: date
# ── SRP Class 1: Fetch/aggregate data ─────────────────────────────────
class SalesDataService:
"""One reason to change: data source or aggregation logic changes."""
def __init__(self, db):
self.db = db
def get_sales(self, from_date: date, to_date: date) -> list[SalesRecord]:
return self.db.query("sales", from_date=from_date, to_date=to_date)
def get_total(self, records: list[SalesRecord]) -> float:
return sum(r.quantity * r.price for r in records)
# ── SRP Class 2: Format data ───────────────────────────────────────────
class SalesReportFormatter:
"""One reason to change: report format or structure changes."""
def format_as_text(self, records: list[SalesRecord], total: float) -> str:
lines = [f"{'Product':<20} {'Qty':>5} {'Price':>10} {'Total':>12}"]
lines.append("-" * 50)
for r in records:
lines.append(f"{r.product:<20} {r.quantity:>5} ${r.price:>9.2f} ${r.quantity*r.price:>11.2f}")
lines.append("-" * 50)
lines.append(f"{'TOTAL':>37}: ${total:>11.2f}")
return "\n".join(lines)
def format_as_dict(self, records: list[SalesRecord], total: float) -> dict:
return {
"records": [{"product": r.product, "qty": r.quantity, "price": r.price} for r in records],
"total": total
}
# ── SRP Class 3: Export/persist ────────────────────────────────────────
class SalesReportExporter:
"""One reason to change: export mechanism or destination changes."""
def export_to_csv(self, records: list[SalesRecord], filepath: str) -> None:
with open(filepath, "w", newline="") as f:
writer = csv.writer(f)
writer.writerow(["Product", "Quantity", "Price", "Date"])
for r in records:
writer.writerow([r.product, r.quantity, r.price, r.sale_date])
print(f"Exported to {filepath}")
def export_to_json(self, data: dict, filepath: str) -> None:
with open(filepath, "w") as f:
json.dump(data, f, indent=2)
# ── Orchestrator: uses all three ───────────────────────────────────────
class ReportController:
def __init__(self, data_svc: SalesDataService,
formatter: SalesReportFormatter,
exporter: SalesReportExporter):
self.data_svc = data_svc
self.formatter = formatter
self.exporter = exporter
def generate_monthly_report(self, year: int, month: int) -> str:
from_date = date(year, month, 1)
to_date = date(year, month, 28)
records = self.data_svc.get_sales(from_date, to_date)
total = self.data_svc.get_total(records)
return self.formatter.format_as_text(records, total)// ─── Java — SRP in a notification system ─────────────────────────────
// Each class has ONE reason to change:
// 1. Template building (Marketing team)
class NotificationTemplate {
private String subject, body;
NotificationTemplate(String subject, String body) {
this.subject = subject; this.body = body;
}
String render(String recipientName) {
return body.replace("{{name}}", recipientName);
}
String getSubject() { return subject; }
}
// 2. Sending logic (Infrastructure team)
class EmailSender {
public void send(String toEmail, String subject, String body) {
System.out.println("Sending email to " + toEmail + ": " + subject);
// SMTP logic here
}
}
// 3. Recipient lookup (DB team)
class RecipientService {
public String getEmail(int userId) {
return "user" + userId + "@example.com"; // DB lookup
}
public String getName(int userId) {
return "User" + userId;
}
}
// 4. Orchestrator (Product team — workflow)
class NotificationService {
private NotificationTemplate template;
private EmailSender sender;
private RecipientService recipientService;
NotificationService(NotificationTemplate t, EmailSender s, RecipientService r) {
this.template = t; this.sender = s; this.recipientService = r;
}
public void notifyUser(int userId) {
String email = recipientService.getEmail(userId);
String name = recipientService.getName(userId);
String body = template.render(name);
sender.send(email, template.getSubject(), body);
}
}
class SRPDemo {
public static void main(String[] args) {
var template = new NotificationTemplate("Welcome!", "Hello {{name}}, welcome aboard!");
var sender = new EmailSender();
var recipients = new RecipientService();
var notifier = new NotificationService(template, sender, recipients);
notifier.notifyUser(42);
}
}// ─── JavaScript — SRP in data processing ─────────────────────────────
// 1. Fetching (API/data concern)
class DataFetcher {
async fetchUsers() {
// In production: return fetch('/api/users').then(r => r.json())
return [{ id: 1, name: "Alice" }, { id: 2, name: "Bob" }];
}
}
// 2. Processing/transformation (business logic concern)
class UserProcessor {
enrichUsers(users) {
return users.map(u => ({
...u,
displayName: u.name.toUpperCase(),
slug: u.name.toLowerCase().replace(' ', '-'),
}));
}
filterActiveUsers(users) {
return users.filter(u => u.id > 0); // simplified
}
}
// 3. Rendering (presentation concern)
class UserRenderer {
renderList(users) {
return users.map(u =>
`<li id="user-${u.id}">${u.displayName}</li>`
).join('\n');
}
renderCount(users) {
return `Total: ${users.length} users`;
}
}
// 4. Orchestrator (workflow concern)
class UserController {
constructor(fetcher, processor, renderer) {
this.fetcher = fetcher;
this.processor = processor;
this.renderer = renderer;
}
async display() {
const raw = await this.fetcher.fetchUsers();
const enriched = this.processor.enrichUsers(raw);
const active = this.processor.filterActiveUsers(enriched);
console.log(this.renderer.renderList(active));
console.log(this.renderer.renderCount(active));
}
}
const controller = new UserController(
new DataFetcher(), new UserProcessor(), new UserRenderer()
);
controller.display();
Key Takeaways
- One class = one reason to change — one actor, one concern.
- SRP reduces coupling: changes in one area don’t cascade into unrelated code.
- Easier to test — small, focused classes are trivially mockable.
- Watch out for God classes — if a class has >5 different types of methods, it likely violates SRP.
- Use dependency injection and composition to wire SRP-compliant classes together.