What is Separation of Concerns in software architecture, and how would you tell whether a system actually follows it?
answer
- concern = one reason a part exists
- one topic per unit, hidden behind an interface
- verify by change test, not folder names
- cohesion up, coupling down, blast radius small
- Dijkstra 1974; Parnas information hiding
basics
~20 sSeparation of Concerns means each part of a system deals with one distinct topic — for example, storing data, applying business rules, or drawing the screen — so you can understand or change one part without touching the others.
solid answer
~50 sA "concern" is a distinct reason a piece of the system exists: persistence, business policy, presentation, authentication, transport. Separation of Concerns says each unit (module, layer, service) should own one concern and hide it behind an interface, so knowledge about that concern lives in exactly one place. The practical test is not "do the folders look tidy" but a change test: pick a plausible change ("swap the database", "change a pricing rule", "add a new UI channel") and count how many modules you must edit. If a single concern's change forces edits scattered across many unrelated files, the concern is not separated — it is smeared. The complementary symptom is a module you must understand two unrelated topics to read (SQL strings interleaved with discount math). SoC drives high cohesion inside a unit and low coupling between units; it is the reason layering, hexagonal ports/adapters, and modular monoliths exist.
code
pseudocode · 13 lines// mixed concerns: transport + rules + storage + notification in one place
function handleCheckout(httpRequest):
body = parseJson(httpRequest.body) // transport
if body.qty <= 0: return http400() // validation
total = body.price * body.qty * 1.21 // business rule (tax)
db.exec("INSERT INTO orders VALUES(...)") // persistence
smtp.send(body.email, "Thanks!") // notification
// separated: each concern behind its own boundary
function handleCheckout(httpRequest):
cmd = CheckoutRequest.parse(httpRequest) // transport concern only
result = checkoutService.place(cmd) // rules; no I/O vocabulary
return HttpPresenter.render(result) // presentation concern onlygo deeper
Define concern, give the classic UI / business-rules / storage split, and say why mixing them makes changes risky and testing hard.
Add the mechanism: modules with interfaces, encapsulation, high cohesion / low coupling, and how it makes business rules unit-testable without a database.
Lead with the verification: the change test and co-change analysis, not folder tidiness. Discuss choosing the right seam and the indirection cost of getting it wrong.
Frame SoC as information hiding of volatile decisions (Parnas), tie seam choice to rate-and-reason-of-change and to team/ownership boundaries, and be explicit about when NOT to separate (invariants that must stay transactional, hot paths, uncertain domains).
## What a "concern" is A **concern** is any distinct topic or reason-to-exist in a system. Typical concerns: - **Domain/business logic** — the rules that make the business the business (how a discount is computed, when an order may ship). - **Persistence** — how data is durably stored and queried (tables, documents, indexes). - **Presentation** — how information is shown and input collected (HTML, mobile screen, CLI output). - **Transport/protocol** — HTTP, gRPC, message queues, serialization formats. - **Infrastructure/technical** — logging, metrics, caching, retries, transactions, authentication. ## The principle **Separation of Concerns (SoC)** — a term popularized by Edsger Dijkstra in his 1974 essay *On the role of scientific thought* — says: when reasoning about a system, focus on one aspect at a time, and *structure the system so that this is possible*. Structurally: each unit of the system should be responsible for one concern, and the details of that concern should be **encapsulated** (hidden behind an interface) so other units depend on *what* it does, not *how*. SoC is scale-free. It applies to a function (one function, one job), a class (Single Responsibility Principle is SoC at class scale), a module, a deployable service, and a whole system topology. ## Why it pays 1. **Localized change.** If pricing rules live in one module, a pricing change is one module's diff — the *blast radius* is small. 2. **Independent reasoning.** You can read the pricing module without knowing SQL dialects. 3. **Substitutability.** A concern hidden behind an interface can be replaced (in-memory store for tests, a different payment provider in production). 4. **Parallel work.** Different people/teams own different concerns with a stable contract between them. 5. **Testability.** Business rules with no I/O in them are testable without a database or network. ## How to *verify* it (the part candidates usually miss) Tidy folder names prove nothing. Use empirical tests: - **The change test / shotgun-surgery test.** Enumerate 5–10 realistic changes. For each, list files touched. A well-separated system shows a *diagonal* pattern: each change hits a small, different set. A poorly separated one shows changes that always hit the same 20 files, or one change hitting 40 files. - **The co-change test.** Mine version-control history: which files change together? Files that always change together are effectively one concern regardless of where they live; files inside one "module" that never change together may be two concerns glued. - **The reading test.** Open a random file. How many distinct vocabularies appear (SQL + currency math + HTTP headers)? Multiple vocabularies in one screen = mixed concerns. - **The dependency test.** Can you draw the dependency graph without cycles, with the domain at the center depending on nothing technical? Cycles usually mean concerns leak both ways. ## Related but distinct ideas (define them so you don't conflate) - **Cohesion** — how strongly the elements *inside* a unit belong together. SoC raises cohesion. - **Coupling** — how much a unit must know about another. SoC lowers it. - **Single Responsibility Principle (SRP)** — Robert Martin's class-level formulation: a class should have one reason to change / serve one actor. SoC is the same instinct at any scale. - **Modularity** — the structural mechanism (modules with interfaces) by which SoC is realized. - **Information hiding** — David Parnas's 1972 rule: modules should hide the *design decisions likely to change*. This is the sharpest operational version of SoC. ## Costs and limits Separation is not free: - **Indirection tax.** Each boundary adds an interface, a mapping, sometimes a DTO and a serialization hop. Over-separation produces "lasagna code" — twelve layers where a function would do. - **Wrong seams are worse than no seams.** If you split along an axis the business never changes along, you pay boundary cost forever and still edit both sides on every change. - **Performance and transactionality.** Concerns split across process boundaries lose in-process calls, shared transactions, and easy joins. The judgment call is *which axis to cut along* — and the best predictor is **volatility**: separate the things that change at different rates or for different reasons. ## A worked example A checkout endpoint that (a) parses JSON, (b) validates the request, (c) computes tax and discounts, (d) writes rows, (e) sends a confirmation email, all in one 300-line function, has five concerns in one place. A tax-rate change risks breaking JSON parsing; an email-provider swap requires touching tax code; the tax logic cannot be unit-tested without a database and an SMTP server. Separating them means a tax change edits one small, pure, fast-tested module.
- How is Separation of Concerns different from the Single Responsibility Principle?They express the same instinct at different scales. SRP is a class-level rule ("one reason to change", one actor served). SoC is scale-free — it applies to functions, modules, services, and whole topologies, and it is the rationale behind layering, ports-and-adapters, and modular monoliths.
- Can a system be over-separated? What does that look like?Yes. Symptoms: every feature change touches 8+ files across 5 layers; interfaces with exactly one implementation that will never have a second; DTO-to-DTO mapping code outweighing logic; a debugger stack 12 frames deep before anything happens. The cost of a boundary is only repaid if that boundary isolates something that actually varies.
- Give a concrete way to measure whether concerns are separated in an existing codebase.Mine the version-control log for co-change: build a matrix of files that change in the same commit. Clusters that cross module boundaries reveal concerns that are split by the folder structure but unified in reality; conversely, files in one module that never co-change hint the module holds two concerns.
A restaurant separates kitchen, dining room, and accounting. Changing the menu doesn't rewire the till; hiring a new accountant doesn't change how steak is cooked. If one person did all three at one counter, every change would interrupt everything.