50%

الدرس 1 من 2

What Counts as a Concern

Separation of concerns is not about folders named controllers, services and repositories. It is about being able to satisfy one reason to change without opening code that exists for another.

قراءة 11 دقيقة

Edsger Dijkstra introduced separation of concerns in 1974 as a way of thinking, not as a directory layout: study one aspect of a problem in isolation, knowing that the aspects must eventually be combined. The engineering version is narrower and more useful — a concern is anything that has its own reason to change, and separating concerns means arranging code so that one reason to change is served in one place.

Notice how close this is to the single-responsibility principle from the first module. They are the same idea at different scales: SRP asks it of a class, separation of concerns asks it of a system. The reason both are worth stating is that a codebase can satisfy one and violate the other. Every class can be beautifully single-purpose while a single feature change still requires edits in nine directories.

Two ways to cut the same system

Slicing by technical layer versus by feature

Technical layers

Top-level folders are `controllers`, `services`, `repositories`, `models`. Every feature contributes one file to each.

  • Immediately familiar to anyone who has used the framework
  • Makes a technology swap within one layer easy to locate
  • Enforces a consistent shape for every feature
  • One feature change touches four directories
  • Nothing in the structure says which files belong together
  • Coupling between features hides easily inside a shared service layer

Choose when: The system is small enough that any developer holds all of it in their head, or the dominant change driver genuinely is technical — replacing the persistence technology across everything, for instance.

Feature slices

Top-level folders are `identity`, `billing`, `catalogue`, each containing its own domain, application, infrastructure and UI code.

  • A feature change is contained in one directory
  • Ownership maps cleanly onto teams
  • A slice can later be extracted, because its edges are visible
  • Cross-slice rules need an explicit, policed mechanism
  • Some technical duplication across slices is unavoidable
  • Requires a decision about what counts as a slice, which is genuinely hard

Choose when: Change requests arrive as features rather than as technology migrations, or more than one team works in the codebase — which covers most systems past their first year.

Where a single feature change lands

The same request — "add a discount code to checkout" — traced through both structures.

In the layered structure the change touches a controller, a service, a repository and a model in four separate directories. In the feature-sliced structure it touches four files inside one checkout directory, and no other slice is opened.

Request

Add discount codes

one business change

Layered structure

controllers/checkout.ts

services/order-service.ts

repositories/order-repo.ts

models/order.ts

Feature structure

checkout/ (four files)

one directory, one owner

every other slice

untouched

The mechanical difference between a folder and a boundary: a public surface plus a rule that everything else is private.typescript
// features/billing/index.ts — the entire public surface of the slice.
export { billingService } from './application/billing-service';
export type { Invoice, InvoiceStatus } from './domain/invoice';

// Allowed: another slice depends on the published contract.
import { billingService } from '@/features/billing';

// Rejected by the dependency rule in CI, even though the file exists:
// import { PrismaInvoiceRepository } from '@/features/billing/infrastructure/prisma-invoice-repository';
//
// The rule is what makes the arrangement survive a deadline. Without it this
// import compiles, works, and quietly couples two slices for years.

In practice

The service layer that was one concern in name only

A team had a textbook layered structure: controllers, services, repositories. Their `OrderService` had grown to 2,400 lines. Every attempt to change pricing broke shipping tests, and nobody could explain why.

Constraints

  • Three teams contributing to the same service layer
  • Full test suite passes, so no obvious defect to chase
  • Change failure rate around one in four releases
  • A rewrite was already being proposed by two of the three teams

Decision

Rather than reorganise the folders, they mapped which methods of `OrderService` were called by which feature, and found three clusters with almost no overlap: pricing, fulfilment and returns. They split the service along those clusters, gave each its own directory and repository, and added an import rule preventing the three from calling each other directly.

Why

The layered structure had separated technical roles while leaving three business concerns tangled inside one class, sharing private helpers and mutable state. The clusters were already visible in the call graph; the folder structure had simply hidden them behind an accurate but useless label.

What it cost

Three repositories now issue three queries where one used to fetch an order in a single round trip, which added roughly 15 milliseconds to the order page. Change failure rate fell to under one in twenty within two quarters. The team judged the latency worth it and recorded the number, so that a future performance push starts from evidence rather than from the memory of a trade-off nobody wrote down.

A codebase has clean controllers, services and repositories, and every class is small and single-purpose. Adding a new field to a customer profile still requires editing eleven files. Is this good separation of concerns?Reveal

No. Each class may satisfy the single-responsibility principle while the system fails separation of concerns, because one reason to change — the shape of a customer profile — is spread across eleven places. The usual cause is that the layers each re-declare the same data in their own form: a request type, a domain type, a persistence type, a response type and several mappers. Some of that redundancy is deliberate and worth paying for, since it stops a database column change from breaking a public API. But eleven files is a signal that the mapping layers exist out of habit rather than because a boundary needed protecting, and the honest next step is to ask which of those five representations is actually protecting something.

Key takeaways

  • A concern is anything with its own reason to change; separating concerns means one reason to change is served in one place.
  • Separation of concerns is the system-scale version of the single-responsibility principle, and a codebase can satisfy one while failing the other.
  • Choose between layer slicing and feature slicing by counting directories touched across your last twenty change requests.
  • A folder is only a boundary if something mechanically rejects an illegal import.
  • Layers can hide tangled business concerns behind accurate technical labels; the call graph is better evidence than the directory tree.