Introduction
Have you seen classes that try to do everything—validate input, calculate business rules, talk to databases, format output, and send notifications? They look convenient at first, but they become fragile, hard to test, and scary to change.
The Single Responsibility Principle (SRP) helps you fight that entropy: a class (or module/function) should have only one reason to change. In practice, that means each unit focuses on a single concern.
The Housekeeping Analogy
Think of a household:
-
One person tries to cook, clean, shop, fix appliances, manage bills, and babysit. They’re overworked; a delay or mistake in one area cascades into others.
-
Contrast that with clear roles: a cook, a cleaner, a shopper, and a handyman. Each role has a single responsibility; if a recipe changes, only the cook adapts. If the vacuum breaks, only the handyman acts.
That’s SRP: separate roles so each change has a single owner.
Why SRP Matters
-
Local change → One change touches one place.
-
Testability → Small units are easier to unit-test and mock.
-
Reusability → Focused classes are portable.
-
Parallel work → Teams can modify different responsibilities safely.
When to Use SRP (Signals & Triggers)
Use SRP when you notice any of these smells:
-
A class changes for multiple reasons (UI, rules, persistence, messaging).
-
Methods cluster around different concerns (validation vs. persistence).
-
Testing requires heavy setup because the class talks to everything.
-
Features are blocked because one giant class is a hotspot for merge conflicts.
Where to Apply SRP (Common Layers)
-
Domain/Model → Keep domain logic (e.g.,
Invoice.calculateTotal
) free from printing, persistence, and transport concerns. -
Application/Service → Orchestrate use cases: call domain logic, repositories, and notifiers—but don’t do their work.
-
Infrastructure → Repositories, HTTP clients, message publishers—keep I/O details out of the domain.
-
Presentation → Controllers/handlers map requests ↔ DTOs; avoid business rules here.
How to Apply SRP (Refactoring Steps)
-
Identify responsibilities in a “God class”.
-
Name the concerns (e.g., Calculation, Persistence, Printing, Notification).
-
Extract classes so each owns exactly one concern.
-
Define interfaces where useful (e.g.,
InvoiceRepository
). -
Inject dependencies (constructor injection) rather than looking them up.
-
Write focused tests per responsibility.
Java Example: SRP Violation (Before)
public class Invoice {
// does calculation, printing, and saving
}
-
Changes in calculation, printing, or persistence all affect this class.
Java Example: SRP Applied (After)
public class Invoice { /* calculation only */ }
public class InvoicePrinter { /* printing responsibility */ }
public class InvoiceRepository { /* persistence responsibility */ }
Now each class has one responsibility.
Orchestration Example
public class InvoiceService {
private final InvoiceRepository repo;
private final InvoicePrinter printer;
public String process(Invoice invoice) {
repo.save(invoice); // persistence
return printer.print(invoice); // presentation
}
}
The service coordinates responsibilities but doesn’t absorb them.
Don’t Overdo It
🚫 Overdone SRP: splitting a simple number into InvoiceAmount
, InvoiceData
, InvoiceCalculator
classes.
✅ Balanced SRP: keep data and calculation in Invoice
, and delegate persistence/printing to separate classes.
When Not to Use SRP
-
Tiny scripts/prototypes where speed matters more than structure.
-
High‑cohesion utilities (e.g., a
Money
object doing arithmetic + formatting). -
Performance‑critical paths where indirection is too costly.
-
Stable legacy code—extract gradually, not all at once.
Quick Checklist for Code Reviews
-
Can I describe this class in one sentence without “and/also”?
-
Will it change for more than one reason?
-
Are business rules mixed with persistence/UI?
-
Do tests require unrelated setup?
-
Can I localize a new change to one class?
Closing Thoughts
Just like a household runs smoother with clear roles, codebases thrive when each class owns one responsibility. Apply SRP where it clarifies intent and isolates change. Avoid the trap of over‑splitting; aim for cohesive, testable, and understandable units.
Mantra: One class, one reason to change.