← Home
🏗Crack System Design with DSA AnimatorSystem DesignNEW
🏗 System Design›SOLID Principles

🎯Single Responsibility Principle (SRP)

A class should have one — and only one — reason to change.

📌 Key IdeaEvery class, module or function should do one well-defined job. If you can describe what a class does using the word "and", split it.
✨

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 EmailService can serve invoices, password resets and marketing — a god Invoice class 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 — InvoicePdfRenderer beats InvoiceManager.
💡

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

❌ Violates SRP — one class, many reasons to change
        ┌─────────────────────────────┐
        │         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
✅ Follows SRP — one reason to change per 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 Money value 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)

God class — every responsibility tangled togetherBadjava
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;
    }
}
One class, four reasons to change. Even a small unit test for calculateTotal() has to mock JDBC, SMTP and the PDF library.
✅

The Right Way

Refactored — one class, one reason to changeGoodjava
// 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);
    }
}
Each kind of change now touches exactly one file. Test the maths without DB, email or PDF in the way.
⚖️

Pros & Cons

👍 Pros
  • 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.
👎 Cons
  • 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

AspectGod Class 🚨SRP Class ✅
Reasons to changeMany (every actor)Exactly one
File size1k+ lines, scrolling fatigue50–200 lines, fits on one screen
Test setupMocks for DB + SMTP + PDF + clockOne or two collaborators
Merge conflictsDaily — every team edits same fileRare — teams own different files
NamingInvoiceManager, Helper, UtilInvoiceRepository, InvoiceRenderer
ReuseZero — too coupled to copyHigh — small, focused, swappable
🔀

Common Confusions Cleared Up

SRP vs "one method per class"

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.

SRP vs Cohesion

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.

SRP vs Separation of Concerns (SoC)

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 User class that has both getName() and setName() 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.)
🧾

Quick Cheat Sheet

TaglineOne class, one reason to change.
TestDescribe the class — if you say "and", split it.
SmellsManager / Helper / Util suffix, > 500 lines, mixed mocks in tests.
Refactor toExtract Class · Extract Interface · Strategy · Decorator.
Don'tSplit every method, force "one verb per class".
🔗

Related Topics