Menu

Earn Premium with Referrals

Invite your friends and earn Premium rewards through our referral program.

See how it works and start inviting friends.

Single Responsibility Principle
LLD

Single Responsibility Principle

Learn how a class should have one clear responsibility and one reason to change.

Single Responsibility Principle (SRP)

The Problem It Solves

OrderService validates orders, persists them, sends confirmation emails, and generates invoices. Three different teams touch it: checkout owns validation rules, platform owns persistence, growth owns email copy. Every change from any team redeploys — and risks — all responsibilities. Merge conflicts, surprise regressions (“email change broke validation?”), and review paralysis follow mechanically.

SRP (Robert C. Martin): a class should have exactly one reason to change — one actor whose requirements it serves.

Seeing Responsibilities

 ONE CLASS, THREE ACTORS                 SPLIT BY REASON-TO-CHANGE

 ┌──────────────────────┐                ┌──────────────┐
 │    OrderService      │                │ OrderValidator│ ← checkout team
 │ validate()           │  each arrow =  ├──────────────┤
 │ save()               │  a different   │ OrderRepository│← platform team
 │ sendConfirmation()   │  actor with    ├──────────────┤
 │ generateInvoice()    │  independent   │ Notifier      │ ← growth team
 └──────────────────────┘  change reasons└──────────────┘

Left: four reasons to change inside one file. Right: each reason isolated; email copy changes can never break invoice generation because they no longer share a compilation unit. The split follows actors, not method counts — a class with five methods serving one actor is SRP-compliant.

Mechanics

// before: three actors' logic entangled
class OrderService {
    void place(Order o) {
        if (o.items().isEmpty()) throw new InvalidOrder();   // checkout's rule
        db.insert(o);                                        // platform's concern
        smtp.send(o.userEmail(), template);                  // growth's copy
    }
}

// after: orchestration only
class CheckoutService {
    private final OrderValidator validator;
    private final OrderRepository repo;
    private final OrderNotifier notifier;

    void place(Order o) {
        validator.check(o);
        repo.save(o);
        notifier.confirmation(o);
    }
}

The orchestrator remains small and stable; each extracted collaborator changes on its own schedule behind its own interface.

What SRP Is Not

MisreadingCorrection
”One method per class”It is about reasons to change (actors), not granularity
”Split everything”Over-splitting creates shotgun-surgery across ten files for one feature
”Only for big classes”A 40-line class serving two actors violates it too

Detection Signals

  • Class name contains Manager, Service, Helper, Processor plus multiple domain verbs.
  • Imports span unrelated domains (SMTP + SQL + PDF in one file).
  • Different teams repeatedly editing the same file.
  • Unit tests needing database + mail server mocks together to test one method.

Interview Application

  • Naming the actor when justifying a split (“this belongs to billing’s rate of change”) scores over mechanical splitting.
  • Follow-ups probe boundaries: “should validation live on Order itself?” — answer via cohesion: rules guarding Order’s invariants belong to Order; cross-entity workflow stays in a service.

Failure Modes

  • Over-extraction into anemic data bags — moving methods off entities without preserving behavior ownership recreates the anti-pattern this principle prevents.

The Situation

A 3,000-line ReportManager: reads requests, parses CSV, computes metrics, formats HTML, emails results, logs to DB, retries failures. Every feature request touches it; every test needs the world mocked; nobody can describe what it is in one sentence. Refactoring it wrong (big-bang rewrite) is how regressions ship — the value of this page is a safe, incremental decomposition path.

The Procedure

 STEP 1            STEP 2              STEP 3             STEP 4
 Map duties   →    Cluster into   →    Extract class  →   Delete old
                                 by responsibility      paths when callers
                                                        migrate
 ┌─────────────────────┐
 │ parse()     ◄─┐    │        Parsing         ReportParser
 │ csvLoad()   ◄─┤ same│       ┌───────────┐   (owns parsing rules)
 │ metrics()   ◄─┼─ grp│       │ extraction│
 │ htmlFmt()   ◄─┘    │       ▼ order    ReportRenderer
 │ email()      alone │    metrics → fmt (owns presentation)
 │ retryLog()   alone │    email/retry → DeliveryService
 └─────────────────────┘

Step 1 — inventory: list every public/private method and field. Step 2 — cluster: group methods that change together and share fields (fields are the strongest cohesion signal — methods reading the same three fields belong together). Step 3 — extract one cluster at a time, tests green between each. Step 4 — retire: once all callers use new classes, delete forwarding shims.

Keeping It Safe

  • Tests first: pin current behavior with characterization tests before moving anything — they become the safety net.
  • One extraction per commit: reviewable units, bisectable regressions.
  • Delegation facade during migration: keep the old class temporarily delegating to new parts so existing callers never break mid-refactor:
// transitional shim — delete after caller migration
class ReportManager {
    private final ReportParser parser = new ReportParser();
    private final DeliveryService delivery = new DeliveryService();
    Report handle(Request r) { return delivery.send(parser.parse(r)); }
}
  • Behavior-preserving rule: no feature changes ride along. “While I’m here” edits destroy bisectability.

Common Pitfalls

PitfallConsequence
Extracting data without behaviorAnemic DTOs + a coordinator that still knows everything
Clustering by layer (all parsers together)New god classes at layer scale
Extracting before writing testsNo way to prove behavior survived
Big-bang split in one PRUnreviewable, un-bisectable

Detection Signals a Split Is Due

  • One sentence cannot describe the class without “and”.
  • Field sets used by disjoint method groups.
  • Import list spans unrelated packages (mail + SQL + templating).
  • Test setup mocks more than two external worlds for one unit.

Interview Framing

  • Asked to “fix this design,” walking the four steps with the actor justification (“these methods change for different teams”) scores higher than redrawing everything from scratch.
  • Mentioning the delegation-facade migration trick signals real-world refactoring experience.

My Private Notes

Notes are auto-saved locally to this device.