When is applying Separation of Concerns harmful? How do you judge that a boundary costs more than it saves?
answer
- boundary = cost: indirection, mapping, contract churn
- separate only on a real axis of variation
- co-change analysis / Common Closure Principle
- reversible (module) before irreversible (service/DB)
- defer: extract on evidence, not speculation
basics
~20 sEvery boundary adds indirection: more files, interfaces and hops to read. If splitting doesn't let the parts change, deploy or be tested independently — and instead makes one change touch many modules — the split costs more than it saves.
solid answer
~50 sA boundary pays off only if the separated parts genuinely vary independently: different rate of change, different stakeholder, different technology, different scaling or failure profile, different team. If they always change together, the boundary buys nothing and charges you indirection, mapping code, interface churn and (across processes) network failure modes, latency, distributed transactions and versioning. Symptoms of over-separation: single-implementation interfaces created "for flexibility"; DTO-to-DTO mappers at every hop; a one-line change spanning six files; layers that only forward calls; nanoservices with chatty synchronous calls. Judgement tools: measure how commits cluster (do files change together?), apply the Common Closure Principle — what changes together stays together; prefer *deferred* separation (build cohesively, extract when a real second variation appears); distinguish reversible boundaries (a module split, cheap to undo) from near-irreversible ones (a service/database split). Rule of thumb: separate on demonstrated variation, not on speculative flexibility (YAGNI); premature boundaries are usually placed wrong and are expensive to move.
go deeper
Say that splitting adds files and indirection, so split when parts really change separately, not just to make files shorter.
List concrete over-separation smells — single-implementation interfaces, pass-through layers, mapper explosion — and the pay-off criteria.
Reason from axes of variation and change history, distinguish reversible from irreversible boundaries, and argue for deferred extraction with enforced module boundaries.
Frame boundaries as hypotheses about future change and as organizational commitments; use co-change data, cost-of-being-wrong asymmetry, and set policy for when a module may become a service.
## The claim Separation of Concerns is a principle, not a law, and it has a cost curve. Under-separation gives you a tangled monolith of a file. Over-separation gives you a system where nothing is complicated but everything is far away. Senior judgement is knowing which side you're on. ## What a boundary actually costs 1. **Cognitive/navigation cost** — understanding one behavior requires opening N artifacts and holding the contract in your head. 2. **Mapping cost** — translating between representations at each boundary (entity → domain model → DTO → view model), which is code with no business value and its own bugs. 3. **Interface churn** — a boundary is a contract; every change to what's needed across it changes the contract in two places (plus tests, plus mocks). 4. **Test seams that lie** — mocking at every boundary yields unit tests that pass while the composed system is broken; you then need integration tests you would not have needed. 5. **Runtime cost, if the boundary is a process** — latency, partial failure, retries, idempotency, no cross-service transactions, schema versioning, deployment ordering, distributed tracing. 6. **Ossification** — a boundary is a commitment. Once other teams depend on it, moving it is a migration project. ## When separation pays Separate when the two sides differ along at least one real axis: | Axis | Example | |---|---| | **Rate of change** | Volatile pricing rules vs. stable currency formatting | | **Reason/actor for change** | Finance-driven vs. DBA-driven (SRP's actor test) | | **Technology / replaceability** | Domain rules vs. a specific database or vendor SDK | | **Testability** | Pure logic that should run without I/O | | **Scaling / failure isolation** | A CPU-heavy report generator vs. a latency-sensitive API | | **Security/compliance** | PII handling isolated for audit scope | | **Team ownership** | Two teams needing independent deploy cadence | | **Reuse** | A rule used by web, batch and CLI entry points | If you cannot name one, you are separating on aesthetics. ## Diagnostic signals of over-separation - **Speculative interfaces**: an interface with exactly one implementation, no test double need, and no plausible second implementation. (Note the nuance: an interface *is* justified when it inverts a dependency direction, as with ports/adapters, or isolates a third-party SDK — even with one implementation.) - **Pass-through layers**: methods that only delegate. - **Mapper explosion**: three structurally identical data shapes per feature. - **Change-set fan-out**: run `git log` and see whether a typical feature commit touches 8 files across 5 directories, always the same ones. Files that always change together want to live together (a real co-change/temporal-coupling analysis). - **Chatty boundaries**: a single use case making many round trips across a process boundary — the boundary was drawn across a cohesive operation. - **Mock-heavy tests** whose setup is longer than the code under test. ## Signals of under-separation - One file/class that many unrelated changes touch (a merge-conflict hotspot). - Business rules impossible to test without a database, browser or network. - The same rule duplicated for each entry point, drifting between copies. - Changing storage technology requires touching business code. ## A decision procedure 1. **Name the concern and the axis of variation.** "Persistence, because we may move from SQL to a document store" or "pricing, because finance changes it monthly". No axis → don't split yet. 2. **Estimate the cost of being wrong in each direction.** Merging two modules later is cheap; splitting a shared database or a public service contract later is expensive. Bias toward the reversible option — separate *inside* a process (modules with enforced boundaries) before separating across processes. 3. **Prefer deferred/evidence-based separation.** Write it cohesively; extract when the second use case, second implementation or second team actually arrives. The extraction is usually easy *if* internal cohesion was kept high; the guess about where the seam goes is what's hard. 4. **Enforce, don't merely declare.** If you do draw a boundary, make violations fail the build (architecture tests, import rules, module systems). An unenforced boundary accrues the cost without the benefit. 5. **Re-evaluate.** Boundaries are hypotheses about future change. Revisit when the change history contradicts them; deleting a useless boundary is real architecture work. ## The framing to give an interviewer SoC is fundamentally about **change-cost management**, following Parnas: modularize around what is likely to change and hide it. "More separation" is not the goal; *matching the boundaries to the actual axes of independent change* is. Both a 3,000-line god class and a constellation of nanoservices fail that test — the first because everything changes together in one place, the second because one change ripples through many places.
- You inherit a service with a single-implementation interface for every class. Is that a problem, and what would you do?It's a smell, not automatically a defect. Keep interfaces that invert a dependency direction (ports over databases, third-party SDK wrappers) or that a genuine second implementation/test double needs. Delete the rest gradually — most IDEs inline them safely — and measure by whether navigation and change size improve. Don't do it as a big-bang refactor.
- How can commit history tell you where the boundaries should be?Co-change (temporal coupling) analysis: files that repeatedly change in the same commit are one concern from the change perspective and probably belong together; clusters that never co-change are candidates for separation. It grounds boundary decisions in evidence rather than taste — the Common Closure Principle made measurable.
- Why is separating into services riskier than separating into modules?A module split is a compile-time, reversible refactor. A service split adds network latency, partial failure, retries and idempotency, independent deployment and versioning, loss of cross-service transactions, and — once another team depends on the contract — a migration project to undo. Prove the boundary as an enforced in-process module first.
Walls in a building. Rooms give privacy and contain fire — but a house where every appliance sits in its own sealed room means cooking dinner is a tour of the building. You put walls where activities genuinely differ, not every three feet.
saying these in an interview costs you the question
- "More layers/interfaces/services is always better design."
- Adding an interface for every class 'in case we swap the implementation' with no candidate second implementation.
- Splitting into services to get modularity, when enforced in-process modules would give the same separation reversibly.
- Judging separation only by file size or class count rather than by whether parts change independently.
- Never revisiting boundaries once drawn, even when the change history contradicts them.