Layered architecture is the classic way to separate concerns. What are its rules and its failure modes, and when would you separate by feature slice instead of by technical layer?
answer
- dependencies point one way (down/inward)
- ports & adapters = dependency inversion
- pass-through layers, change amplification, leaks
- package-by-feature > package-by-layer
- slice vertically, layer inside the slice
basics
~20 sLayering splits code into presentation, business logic and data access, with dependencies pointing one way. It fails when every small change touches all layers, when layers just pass data through, or when database concerns leak upward. Splitting by feature keeps related code together instead.
solid answer
~50 sA layered architecture groups modules by technical role — presentation/API, application, domain, infrastructure/persistence — with a strict rule that dependencies point in one direction (downward, or in hexagonal/clean variants inward, with infrastructure depending on domain interfaces via dependency inversion). It separates the concerns that change for different technical reasons: swapping a database or a UI ideally touches one layer. Failure modes: **pass-through/anemic layers** that only forward calls; **change amplification**, where a trivial field addition edits DTO, mapper, service, entity and migration; **leaky layers**, where persistence idioms (lazy loading, ORM entities, SQL paging) surface in the UI; **skipped layers**, quietly turning the design into a big ball of mud; and layers modeling *technical* concerns while teams and requirements are organized around *features*. Vertical slicing (feature/module folders, modular monolith, bounded contexts) separates by business capability, keeps things that change together together (Common Closure Principle), and usually layers *inside* each slice. Common answer: slice vertically first, layer within the slice.
go deeper
Describe the layers and the one-way dependency rule, and give one benefit (swap the database without touching business rules).
Add dependency inversion via ports/adapters, plus concrete failure modes: pass-through layers, leaked ORM entities, skipped layers.
Discuss change amplification, pick the separation axis from actual change patterns, contrast package-by-layer vs package-by-feature, and mention automated boundary enforcement.
Tie module boundaries to team topology and Conway's Law, plan slice-to-service extraction paths, define shared-kernel policy, and set org-wide enforcement in CI.
## What layering is **Layered (n-tier) architecture** partitions a system by technical role: 1. **Presentation / API** — HTTP controllers, UI, serialization, request/response models. 2. **Application / use-case** — orchestration, transaction boundaries, authorization entry points. 3. **Domain** — entities, value objects, business rules and invariants. 4. **Infrastructure / persistence** — database access, message brokers, external HTTP clients, file systems. Two rules make it separation rather than decoration: - **Directed dependencies.** A layer may depend only on the layer(s) below (classic) or only inward (hexagonal / ports-and-adapters / clean architecture). No cycles, no upward references. - **Contracts at the boundary.** Each layer exposes an interface; internals stay private. In the inward-dependency variants, the domain defines *ports* (interfaces) and infrastructure supplies *adapters*, so the compile-time dependency points from infrastructure to domain — **dependency inversion**. That is what makes the domain testable without a database. A further distinction: **open vs. closed layers.** In a closed layered architecture every call passes through each layer in order (better isolation, more pass-through code); open layers permit skipping (less ceremony, weaker isolation). Choose deliberately and state the rule — an undocumented mix decays fast. ## What layering buys - Replaceable I/O: change ORM, database, or transport with contained edits. - Fast tests: domain logic runs in memory. - Uniform placement rules — newcomers know where code goes. - A natural home for cross-cutting concerns (transaction boundary at the application layer, authn at the presentation edge). ## Failure modes (what interviewers want) ### 1. Pass-through / "lasagna" layers Every layer's method does `return next.doTheSameThing(args)`. The indirection buys nothing and multiplies files. Symptom: a service class with only single-line delegations and a mapper that copies identical fields. Remedy: collapse the layer, or justify it with an actual transformation/policy it applies. ### 2. Change amplification Adding one field touches: API DTO → validation → mapper → application command → domain entity → persistence entity → mapper → migration → test fixtures. If the vast majority of your changes are feature-shaped and each one hits every layer, then your separation axis is orthogonal to your change axis. This is the strongest empirical argument for vertical slicing. ### 3. Leaky layers The domain imports ORM annotations; controllers receive entities and trigger lazy loading during serialization; UI paging is expressed in SQL `OFFSET`. The layer boundary exists syntactically but the concern crossed it. Detect with dependency rules enforced in CI (import checks, ArchUnit-style tests, module systems, linters). ### 4. Skipped layers / cycles A controller calling a repository directly "just this once". Without automated enforcement, that becomes the norm and the architecture is documentation only. ### 5. Anemic domain All behavior lives in "services", entities are field bags. Technically layered, but the domain concern isn't actually separated — it's scattered across procedural service classes. ### 6. Layers as team boundaries A UI team, a backend team and a DBA team means every feature needs three teams and three handoffs — Conway's Law working against you. Cross-functional teams owning vertical slices deliver features independently. ## Vertical slicing / feature-based separation Organize top-level modules by **business capability** — `orders/`, `billing/`, `catalog/` — each containing its own controller, use cases, domain and persistence. Related ideas: package-by-feature (vs package-by-layer), modular monolith, bounded contexts in Domain-Driven Design, and the Common Closure Principle: *classes that change together belong together.* Benefits: a feature change lives in one directory; slices can be extracted into services later; ownership maps to teams; deletion of a feature is a directory delete. Costs: shared concepts risk duplication across slices; you need explicit rules for cross-slice communication (public API per module, events, no reaching into another slice's internals — enforceable with module systems or architecture tests); and the boundaries are harder to draw correctly early. ## The usual synthesis Slice **vertically by capability at the top level**, and layer **inside each slice**. You then get both separations along the axes that matter: features change independently of each other, and within a feature the domain rule stays independent of transport and storage. Keep genuinely shared kernel code small and explicitly shared. ## Deciding: questions to ask - Look at the last 50 commits: do they cluster by feature or by layer? Separate along the dominant clustering. - Which is more likely — replacing the database, or shipping a new feature this quarter? Optimize for the frequent case. - How many teams touch this codebase, and along what lines? - Is any layer purely mechanical? If it never applies a policy or transformation, it is overhead. - Are the layer rules enforced automatically, or aspirational? Unenforced boundaries erode.
- In hexagonal / clean architecture, why does infrastructure depend on the domain rather than the other way round?The domain defines ports (interfaces) it needs, e.g. `OrderRepository`; infrastructure supplies adapters implementing them. The compile-time arrow therefore points inward, so the domain compiles and tests without any database or framework, and I/O technology can be swapped without touching business rules. This is the Dependency Inversion Principle applied at architectural scale.
- How do you stop layer or module boundaries from eroding over time?Automate enforcement: architecture tests (ArchUnit-style), dependency-cruiser/import lint rules, language module systems or build-module boundaries so illegal imports fail the build. Documentation alone always loses to deadline pressure.
- When is a strictly layered design still the better choice over vertical slices?When the system is small or has one cohesive capability, when the dominant risk really is replacing an I/O technology, or when the team is small and the extra module ceremony would not pay off. Also for libraries whose 'features' are not independent business capabilities.
A hospital could be organized by profession (all surgeons on floor 1, all nurses on floor 2, all pharmacists on floor 3) or by care pathway (a cardiac unit with its own surgeons, nurses and pharmacy). Layering is the first; vertical slicing is the second. The patient (feature) has a much shorter path in the second layout — but each unit needs its own version of shared functions.
saying these in an interview costs you the question
- Claiming more layers always means better separation, ignoring pass-through cost and change amplification.
- Letting ORM entities or ORM annotations flow into controllers/UI and still calling the layers separated.
- Treating layer boundaries as conventions with no automated enforcement.
- Assuming vertical slicing means no layering at all — usually you layer inside each slice.
- Organizing teams by layer and expecting features to ship independently.