skip to content

When does horizontal technical layering (presentation/domain/infrastructure) become a liability, and what structural alternatives — such as vertical feature slices or modular decomposition — would you choose instead?

level: principalimportance: should knowfreq 38%

answer

  1. group by role vs. group by feature
  2. change is feature-shaped, layers are role-shaped
  3. modules first, layers inside modules
  4. layered ball of mud is possible
  5. carve boundaries along observed change coupling

basics

~20 s

Layering groups code by technical role, so one feature is spread across many folders and most changes touch every layer. When features change independently and teams own features, grouping by feature (vertical slices or modules) — with layering applied inside each slice — usually beats a single global layer stack.

solid answer

~60 s

Horizontal layering optimizes for swapping a *technology* and gives every kind of code a home. Its weakness is **axis of change**: real change requests are feature-shaped ("add a discount rule"), so a global layer stack forces edits in five directories and gives no module boundary around a feature. As a system grows, the layer packages become giant, ownership blurs, and the only enforced boundary is technical. Alternatives: **vertical slices** (a package per use case containing its own handler, logic, and data access), **modular/bounded-context decomposition** (a module per business capability with a published API and internal layers hidden), and **hexagonal/clean** (an inward-pointing variant of layering rather than a replacement). Microservices are the deployment-level version of modular decomposition and add distribution cost. My default for a nontrivial system: partition first by business capability, then layer *inside* each module, keep module-to-module contact on explicit APIs or events, and enforce it in the build. Keep flat global layering for small systems, thin CRUD services, and cases where the team is one team and the domain has few rules.

go deeper

for a junior

Say layering groups by technical role, so one feature is spread across several folders; grouping by feature keeps related code together.

for a middle

Give concrete symptoms (five-directory edits, giant service packages) and describe vertical slices and feature modules as alternatives.

for a senior

Compare styles on change locality, ownership, and extraction cost; recommend modules with layering inside and explain how to enforce it.

for a principal

Drive the decision from evidence (change coupling, team topology, defect clustering), sequence a migration, address shared data and cross-cutting concerns, and insist that whichever structure is chosen is enforced by tooling and fitness functions.

## Two partitioning axes Every structural decision starts with: what do we group by? - **Horizontal / technical layering** groups by *what the code is* — controllers together, services together, repositories together. - **Vertical / functional partitioning** groups by *what the code is for* — everything about "checkout" together, everything about "billing" together. Both can be right; they optimize different things. Layering optimizes replacing a technology and gives a uniform template. Vertical partitioning optimizes changing a feature and gives ownership boundaries. ## Where layering starts to hurt 1. **Change locality.** Requests arrive shaped like features. Under global layering, one feature edit touches controller, DTO, mapper, service, repository, and schema — in five directories owned by nobody in particular. Vertical grouping puts those edits in one place. 2. **No feature boundary.** Layering only enforces *technical* boundaries. Nothing stops the `OrderService` from calling `InventoryService`, or one team's logic from sprawling into another's. You can have a perfectly layered big ball of mud. 3. **Scale and ownership.** A `services` package with 400 classes has no meaningful owner. Modules aligned to capabilities map to teams (Conway's law works with you rather than against you). 4. **Uniformity tax.** Layering imposes one shape on everything: a trivial lookup gets the same five hops as a rules-heavy workflow. Vertical slices let each use case be as thick or thin as it needs to be. 5. **Extraction cost.** If you ever need to split out a service, capability-aligned modules extract cleanly; layer-aligned code has to be untangled feature by feature first. 6. **Coupling shape.** The most damaging couplings in large systems are between features, and layering neither reveals nor restricts them. ## The alternatives ### Vertical slice architecture One package per use case (`PlaceOrder`), containing its request/response types, handler, rules, and data access — often with no shared service layer at all. Pros: extreme change locality, minimal indirection, each slice can pick the simplest mechanism (a hand-written query for a report, a rich model for a rules-heavy command). Cons: duplication across slices if you are undisciplined; shared invariants need a deliberate home; less uniform for newcomers; refactoring tools and conventions in some frameworks fight it. ### Modular decomposition / bounded contexts (modular monolith) A module per business capability, each with a **published API** (a small surface other modules may call) and everything else internal — including its own internal layering and its own tables. Cross-module contact via API calls or events; no reaching into another module's internals or database tables. Pros: real ownership, extractable to services later, keeps a single deployment's simplicity. Cons: requires enforcement (module systems, architecture tests) and up-front context boundaries you may get wrong; shared data needs explicit contracts. ### Hexagonal / ports-and-adapters, onion, clean Not a replacement for layering — a *reorientation* of it: the domain sits at the center, and everything technical depends inward via ports. Combine freely with either axis. ### Microservices Modular decomposition with independent deployment. Adds network latency, partial failure, distributed data, versioning, and operational cost. Choose it for independent deployability, scaling, or team autonomy — never merely to get boundaries you could have enforced in-process. ### Pipeline, event-driven, space-based, and other styles Worth naming as a principal: pipes-and-filters for transformation workloads, event-driven for high decoupling and asynchronous fan-out, each with different failure and observability profiles. The question "layering vs. X" is really "which coupling do I most need to control?" ## Combining rather than choosing The practical answer is usually **both axes, in a defined order**: partition first by capability/module, then apply layering (ideally inverted, domain at the center) *inside* each module. That gives feature-local change plus a disciplined internal dependency graph. Cross-cutting infrastructure (persistence helpers, HTTP plumbing) lives in shared technical modules that domain modules depend on through their own ports. ## When flat layering is still the right call - Small systems and small teams, where one folder per role is genuinely navigable. - Thin CRUD or reporting services with few rules — the domain layer would be a sinkhole anyway. - Early-stage products where capability boundaries are not yet knowable; premature module boundaries are expensive to move. Start layered, watch which parts change together, and carve modules along observed change lines. ## How to decide and defend it Use evidence, not taste: look at commit histories for change coupling (files that always change together belong together), at the number of teams touching each area, at defect clustering, and at the cost of the next planned change. Then encode whichever structure you chose so the build enforces it, and add fitness functions so drift is visible. The failure mode at principal level is not picking the wrong style — it is picking one and leaving it unenforced.

  • How would you migrate a large, flat, layer-partitioned codebase toward capability modules without a big-bang rewrite?
    Incrementally and evidence-led. Identify candidate boundaries from change coupling and team ownership, carve one module at a time by moving its classes into a module with a small published API, replace direct cross-boundary calls with that API or events, and add an enforced dependency rule so the new boundary cannot be re-crossed. Untangle shared database tables last, since that is the slowest step; keep the old structure working throughout.
  • If each vertical slice owns its own data access, how do you prevent duplicated business rules across slices?
    Keep invariants in shared domain types that slices use, rather than in shared service layers slices call for everything. Duplication of *mechanism* (a query, a mapping) is usually cheap and acceptable; duplication of a *rule* is not. Review for repeated decisions, and when the same rule appears twice, extract it into the domain model owned by the relevant capability.

A hardware store arranged by material — all metal together, all plastic together — is easy to restock but terrible for the customer who wants to fix a door: hinges, screws, and handles are in three aisles. Arranging by job (a door aisle) matches how work actually arrives, while each aisle can still be neatly ordered inside.

saying these in an interview costs you the question

  • Presenting layering and hexagonal architecture as alternatives — hexagonal is layering with the dependencies inverted.
  • Assuming feature-based structure means abandoning any dependency discipline inside a feature.
  • Adopting microservices to obtain boundaries that in-process modules would enforce for free.
  • Claiming layering prevents a big ball of mud; a perfectly layered system can still be feature-tangled.
  • Deciding the split from personal preference rather than change-coupling, ownership, and defect data.
  • Drawing module boundaries up front in a domain nobody understands yet, then treating them as immovable.

context