skip to content

When is applying the Single Responsibility Principle harmful, and how do you decide that a module with several capabilities should be left as it is?

level: principalimportance: should knowfreq 41%

answer

  1. boundaries cost indirection, wiring, contracts, coordination
  2. over-split → shotgun surgery, anemic, ravioli
  3. speculative split = YAGNI for structure
  4. one service per entity → distributed monolith
  5. reversibility flips: classes cheap to merge, services are not

basics

~20 s

SRP is harmful when you split by guesswork. Extra classes add indirection, wiring and reading cost. If one team owns all the behavior and changes always arrive together, leaving it as one module is the cheaper, clearer design.

solid answer

~50 s

SRP prices a boundary against expected change. If a module's capabilities are requested by one actor and historically change together, splitting buys nothing and costs indirection, navigation, wiring, and lost local reasoning — the "shotgun surgery" and anemic-design failure modes. Decide with evidence: does version history show distinct change streams? Do distinct teams own them? Is the code volatile or effectively frozen? Would the split create a distributed transaction or chatty call path (especially at service granularity, where SRP misapplied as one-service-per-entity yields a distributed monolith)? Also weigh reversibility: merging over-split code is usually easier than splitting an entangled one at class level, but at service level the asymmetry reverses, so prefer coarse services and split later on evidence. Deliberate deferral — keep it together until a second actor appears — is the mature position, provided the module stays internally organized so the seam is cheap to cut when it does.

go deeper

for a junior

Say that splitting has a cost — more files, harder to follow — and that you should not split before you know changes really arrive separately.

for a middle

Name concrete over-application smells (shotgun surgery, anemic model) and use "do these change together?" as the deciding question.

for a senior

Add evidence-based decision inputs (change coupling, volatility, ownership), the invariants/transaction/performance reasons to keep parts together, and how to keep a deferred seam cheap.

for a principal

Frame boundaries as priced investments with reversibility asymmetry, connect to Conway's law and team ownership, warn about SRP-as-one-service-per-entity producing a distributed monolith, and set an organizational policy for when boundaries get created.

## The cost side of the ledger Every boundary you create must be paid for: - **Indirection cost** — reading behavior now requires jumping across files; the "where does this actually happen?" tax. - **Wiring cost** — constructors, interfaces, dependency-injection registrations, factories. - **Contract cost** — an internal method call becomes a published contract you must keep stable, version, and document. - **Lost local reasoning** — invariants that were obvious when the state sat in one place now need explicit guards on both sides. - **Coordination cost** — at module/service scale, a boundary between teams means meetings, API reviews, and release sequencing. SRP is worth these costs only when it buys real **change isolation**. ## Failure modes of over-application 1. **Shotgun surgery** — the opposite smell of a god class: one business change forces small edits in many places. Over-splitting manufactures it directly. 2. **Anemic domain model** — data in dumb structures, behavior smeared across "service" classes; invariants no longer enforced by the type that owns the data. 3. **Ravioli code** — hundreds of tiny classes with no narrative; the system is comprehensible only in aggregate, which is to say not comprehensible. 4. **Speculative decomposition** — splitting along an imagined future axis. When the real change arrives on a different axis, you now fight both the old design and its scaffolding. This is YAGNI ("You Aren't Gonna Need It") applied to structure. 5. **Distributed monolith** — SRP taken to service granularity as "one service per entity": every request fans out over the network, transactions become sagas, and the coupling is unchanged but now with latency and partial failure. ## The decision procedure Ask, in order: 1. **Who requests changes to each capability?** One actor → strong default: keep together. 2. **What does history say?** Do the capabilities appear in the same commits (change coupling)? Coupled history → keep together. Independent streams → split. 3. **How volatile is it?** Frozen code gains nothing from better boundaries; volatile code gains the most. 4. **Who owns it?** A boundary that does not follow team ownership creates coordination without autonomy (Conway's law: system structure mirrors communication structure — boundaries misaligned with teams get eroded or become bottlenecks). 5. **What breaks if I split?** Transactional consistency, an invariant across the two parts, a hot path's performance, or a chatty call graph are all strong reasons to keep the parts together. 6. **How reversible is the decision?** At class level, merging over-split code is usually cheap and splitting entangled code is expensive — so a slight bias toward splitting is safe. At **service** level the asymmetry flips: extracting a service later is far cheaper than un-splitting a distributed system, so the correct bias is coarse-first, split on evidence. ## Deliberate deferral done well "Leave it together" is not the same as "leave it messy". Keep the seam cheap to cut: - keep the capabilities in separate, clearly named functions/sections inside the module, - avoid shared mutable state across the prospective seam, - do not let unrelated helpers be shared between the prospective halves, - keep tests grouped per capability. Then the day a second actor appears, the split is a mechanical extraction rather than an archaeology project. This is often called the **Rule of Three** applied to abstraction: wait for the third instance / second actor before committing to the boundary. ## Cases where a large multi-capability module is legitimate - A **compiler front end**, parser, or protocol codec with many methods but one grammar and one owner. - A **state machine** whose transitions must be seen together to be verified. - **Performance-critical** code where fusing avoids allocation and virtual dispatch — deviate deliberately and document why. - **Value types** with a wide but coherent method surface. ## How to phrase this in an interview Avoid both extremes. The strong answer is: *SRP is a heuristic for pricing boundaries against expected change; I apply it where evidence of distinct actors exists, I keep seams cheap where it does not, and I bias coarse at service granularity because that split is far harder to undo.*

  • You inherit code split into dozens of tiny single-method classes, and every feature edits ten of them. What do you do?
    Measure change coupling from history, then re-merge the pieces that always change together into cohesive modules named for the behavior, keeping only the boundaries that separate genuinely distinct change sources or isolate slow/external dependencies.
  • Why is the split/keep-together bias different for classes than for microservices?
    Reversibility. Merging over-split classes is a cheap local refactor, so erring toward splitting is low-risk. Un-splitting services means undoing network contracts, data ownership, deployment pipelines, and sagas, so services should start coarse and split only on demonstrated evidence.

Walls in a building. Walls give privacy and fire containment, but each one costs floor space and doorways. You build a wall where two different tenants live, not between every desk.

context