Single Responsibility Principle (SRP)
A class should have one — and only one — reason to change.
Why It Matters
- Localized change — Edits stay in one file → fewer regressions, smaller diffs, faster code review.
- Easier testing — Smaller classes have fewer collaborators → easier to mock and unit-test.
- Better reuse — A focused
EmailServicecan serve invoices, password resets and marketing — a godInvoiceclass can't. - Lower merge conflict — Different actors → different files → different teams can ship in parallel.
- Sharper naming — When a class does one thing, you name it after the thing —
InvoicePdfRendererbeatsInvoiceManager.
In Layman's Terms
Think of a Swiss Army knife versus a chef's kitchen. A Swiss Army knife is fun for camping, but no chef cooks dinner with one — they reach for the right knife, the right pan, the right whisk.
SRP says the same thing about classes: don't make one Swiss Army class that cooks, plates, serves and washes the dishes. Give each tool one purpose so when the menu changes, only the relevant tool changes.
Real-World Analogy
🏥 Hospital Analogy
Imagine a hospital where the same person is the receptionist, doctor, pharmacist and accountant. If the billing rules change, that person stops treating patients to update spreadsheets. If a new medicine arrives, they pause surgery to re-stock. Chaos.
A real hospital separates these roles. The receptionist changes when admission rules change; the pharmacist changes when medicines change; the accountant changes when GST rates change. Each "actor" has one person dedicated to them — that's SRP applied to people.
Visual
┌─────────────────────────────┐
│ Invoice │
├─────────────────────────────┤
│ + calculateTotal() │ 💰 Business rules
│ + saveToDatabase() │ 💾 Persistence
│ + sendEmail() │ 📧 Notification
│ + printPdf() │ 🖨 Presentation
│ + log() │ 📜 Logging
└─────────────────────────────┘
⤴ change in DB → touch this class
⤴ change in PDF → touch this class
⤴ change in email → touch this class ┌──────────────┐ ┌──────────────────┐
│ Invoice │──────▶│ InvoiceCalculator │ 💰 totals
└──────────────┘ └──────────────────┘
│
├──▶ ┌──────────────────┐
│ │ InvoiceRepository│ 💾 persistence
│ └──────────────────┘
│
├──▶ ┌──────────────────┐
│ │ EmailService │ 📧 notification
│ └──────────────────┘
│
└──▶ ┌──────────────────┐
│ InvoicePrinter │ 🖨 PDF/Print
└──────────────────┘Same domain (Invoice). On the left a "god class". On the right, responsibilities split per actor.
When to Use
- Any time a class touches two unrelated stakeholders (e.g., business rules + persistence).
- When a class is asked to log, send email, format JSON and compute totals.
- When test files for a class need mocks for unrelated systems (DB + SMTP + filesystem).
- When the class name contains "Manager", "Helper", "Util", "Processor" — usually a smell.
- When git history shows the same file edited by every team for unrelated reasons.
When NOT to Use
- Tiny scripts, one-off migrations or throwaway prototypes — splitting adds ceremony with no payoff.
- When two responsibilities are truly inseparable for the domain (e.g., a
Moneyvalue object that holds amount + currency — those *change together*). - Don't over-split into one-method classes. SRP is about reasons to change, not method count.
Anti-Pattern (What NOT to Do)
public class Invoice {
private final List<LineItem> items;
private final Customer customer;
public Invoice(Customer c, List<LineItem> items) {
this.customer = c;
this.items = items;
}
// 💰 Business rule — Accounting cares
public BigDecimal calculateTotal() {
BigDecimal sum = BigDecimal.ZERO;
for (LineItem li : items) sum = sum.add(li.getAmount());
return sum.add(sum.multiply(new BigDecimal("0.18"))); // GST
}
// 💾 Persistence — DBA cares
public void saveToDatabase() {
try (Connection c = DriverManager.getConnection(JDBC_URL)) {
PreparedStatement ps = c.prepareStatement(
"INSERT INTO invoices(...) VALUES(...)");
ps.executeUpdate();
} catch (SQLException e) { throw new RuntimeException(e); }
}
// 📧 Notification — Marketing cares
public void emailToCustomer() {
Properties p = new Properties();
p.put("mail.smtp.host", "smtp.acme.com");
Session s = Session.getInstance(p);
Message m = new MimeMessage(s);
m.setRecipients(TO, InternetAddress.parse(customer.email()));
Transport.send(m);
}
// 🖨 Presentation — Designer cares
public byte[] renderPdf() {
PdfDocument doc = new PdfDocument(new PdfWriter(new ByteArrayOutputStream()));
// ...layout, fonts, page breaks
return /* bytes */ null;
}
}calculateTotal() has to mock JDBC, SMTP and the PDF library.The Right Way
// 1. Pure data + business logic only — Accounting's responsibility.
public class Invoice {
private final List<LineItem> items;
private final Customer customer;
public Invoice(Customer c, List<LineItem> items) {
this.customer = c;
this.items = items;
}
public BigDecimal subtotal() {
return items.stream()
.map(LineItem::amount)
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
public BigDecimal total(TaxPolicy tax) {
return tax.apply(subtotal());
}
public Customer customer() { return customer; }
public List<LineItem> items() { return List.copyOf(items); }
}
// 2. Persistence — DBA's responsibility.
public interface InvoiceRepository {
void save(Invoice invoice);
Optional<Invoice> findById(UUID id);
}
// 3. Notification — Marketing's responsibility.
public interface InvoiceNotifier {
void notifyCustomer(Invoice invoice);
}
// 4. Presentation — Designer's responsibility.
public interface InvoiceRenderer {
byte[] toPdf(Invoice invoice);
}
// 5. Orchestration — pulls collaborators together.
public class InvoiceService {
private final InvoiceRepository repo;
private final InvoiceNotifier notifier;
private final InvoiceRenderer renderer;
public InvoiceService(InvoiceRepository r, InvoiceNotifier n, InvoiceRenderer p) {
this.repo = r; this.notifier = n; this.renderer = p;
}
public byte[] checkout(Invoice inv) {
repo.save(inv);
notifier.notifyCustomer(inv);
return renderer.toPdf(inv);
}
}Pros & Cons
- Change isolation — fewer files touched per ticket.
- Trivial unit tests — mock one collaborator at a time.
- Reusable building blocks across features.
- Clearer ownership — Accounting owns Invoice, DBA owns repository.
- Lower cognitive load — open a file, instantly know what it does.
- More files / packages to navigate — IDE search becomes important.
- Risk of over-splitting into anemic, one-method classes ("class explosion").
- Indirection cost — junior devs must hop through interfaces to read flow.
- You must agree on who the actor is — the boundary is judgement, not a rule.
God Class vs SRP-respecting Class
| Aspect | God Class 🚨 | SRP Class ✅ |
|---|---|---|
| Reasons to change | Many (every actor) | Exactly one |
| File size | 1k+ lines, scrolling fatigue | 50–200 lines, fits on one screen |
| Test setup | Mocks for DB + SMTP + PDF + clock | One or two collaborators |
| Merge conflicts | Daily — every team edits same file | Rare — teams own different files |
| Naming | InvoiceManager, Helper, Util | InvoiceRepository, InvoiceRenderer |
| Reuse | Zero — too coupled to copy | High — small, focused, swappable |
Common Confusions Cleared Up
SRP is not about method count. A RateLimiter with five private helpers and one public boolean allow(Key k) still has one responsibility (rate limiting) and is perfectly SRP-compliant.
High cohesion means class members work together towards one goal. SRP is the operational test for cohesion: *if two members would change for different reasons, cohesion is fake.* SRP is cohesion measured against actors, not against a feeling.
SoC is about layers / cross-cutting concerns at the architecture level (UI vs domain vs storage). SRP is the same idea applied at the class level. SRP is "SoC, but per class".
Common Mistakes / Anti-Patterns
- Classes named Manager / Helper / Util / Processor — the suffix hides multiple responsibilities.
- A `saveAndNotify()` method — the word "and" is a red flag.
- Static "service locator" classes that everyone imports — they accumulate every concern.
- Splitting too far so a single feature change requires editing 8 files (over-correction).
- Treating layers (Controller / Service / Repository) as if SRP is "satisfied" — within a layer, multiple actors can still hide.
Interview Q&A
Likely Follow-up Questions
- How would you refactor a 2,000-line
OrderService? (look for actors → group methods → extract collaborators → introduce interfaces). - How does SRP relate to microservices? (microservices apply SRP at the service boundary — one service per bounded context).
- How does SRP interact with the Open/Closed Principle? (SRP gives you the right seams; OCP keeps them stable when extending).
- Would a
Userclass that has bothgetName()andsetName()violate SRP? (No — both serve the same actor and the same reason to change). - What design patterns help you achieve SRP? (Strategy for varying behaviour, Decorator for cross-cutting concerns, Facade for orchestration.)