Layered / N-Tier Architecture
Layered architecture organizes code into horizontal layers — commonly presentation, application/domain, and data access — with a dependency rule that higher layers call lower layers, not the reverse. N-tier often maps those layers to separately deployable tiers (browser, app servers, database).
It is widely taught and easy to explain. It helps when teams need clear technical separation and auditability. It hurts when business complexity is the main problem and layering becomes ceremony that scatters domain logic.
Presentation / UI / API
HTTP controllers, GraphQL resolvers, CLI. Translates external protocols to application calls. No business rules beyond input validation shaping.
Application / use case
Orchestrates domain operations, transactions, authorization checks at use-case level.
Domain
Business rules, entities, domain services. Should not depend on frameworks or SQL.
Persistence / infrastructure
ORM, file storage, external HTTP clients. Implements interfaces defined inward.
Classic strict layering sometimes collapses domain into "services" that are anemic — entities as data bags, logic in service classes. Prefer keeping invariants in the domain model when complexity warrants it.
- Dependencies point inward/downward: UI → application → domain ← infrastructure adapters.
- Lower layers must not import UI types.
- Domain must not import ORM annotations if you want framework independence (hexagonal strengthens this).
- Cross-cutting concerns (logging, auth) via explicit mechanisms — not random imports upward.
| 1 | Allowed: api → app → domain |
| 2 | Allowed: infrastructure → domain (implements ports) |
| 3 | Forbidden: domain → api |
| 4 | Forbidden: domain → orm driver directly (prefer ports) |
note
| Situation | Why layers help |
|---|---|
| Large junior-heavy team | Clear place for new code |
| Audit / compliance mapping | Controls mapped to layers |
| Stable CRUD systems | Predictable structure |
| Separating UI from data access | Replace presentation without rewriting SQL |
- Domain is complex — horizontal slices scatter a single business change across many folders.
- Every feature requires touching all layers mechanically (pass-through methods with no logic).
- Layers become dumping grounds ("UtilService", "CommonManager").
- Physical N-tier distribution without need adds latency and failure modes.
warning
- Keep validation of business invariants in domain, not only controllers.
- Do not let controllers access repositories directly if you need a use-case boundary.
- Combine with vertical modules: layered inside each module, not one global layer cake.
- Enforce with lint/arch tests.
- Document the dependency rule in an ADR.
| 1 | // Application layer orchestrates; domain enforces rules |
| 2 | export async function placeOrder(cmd: PlaceOrder, deps: {{ |
| 3 | orders: OrderRepository; |
| 4 | prices: PricingPort; |
| 5 | events: EventPort; |
| 6 | }}) {{ |
| 7 | const total = await deps.prices.quote(cmd.items); |
| 8 | const order = Order.create(cmd, total); // domain invariants |
| 9 | await deps.orders.save(order); |
| 10 | await deps.events.publish("OrderPlaced", order.id); |
| 11 | return order.id; |
| 12 | }} |
Logical layers need not map 1:1 to network tiers. Many systems keep presentation+application+domain in one process and treat DB as the second tier. Adding tiers requires requirements: separate scale, separate security zones, or separate teams managing each tier.
| Requirement | Tier implication |
|---|---|
| Browser + API + DB only | Classic 3-tier is enough |
| DMZ / internal segmentation | Extra network tiers / gateways |
| Independent scale of rendering | Separate BFF or SSR tier |
Controllers should translate and authorize at the edge, then call application use cases. Repositories encapsulate queries. Domain objects enforce invariants. DTOs crossing layer boundaries prevent leaking persistence models to APIs.
| Layer | Does | Does not |
|---|---|---|
| Presentation | Map HTTP ↔ DTO; authn context | Encode pricing rules |
| Application | Transaction boundary; orchestration | Raw SQL everywhere |
| Domain | Invariants; policies | Know Express/Django |
| Infrastructure | IO details | Decide business outcomes |
Many codebases start layered and grow painful. Evolve by introducing vertical modules while keeping layers inside each module — a modular layered monolith. Alternatively, push toward hexagonal ports at the domain boundary when framework coupling hurts.
- Identify the hottest change sets that touch every layer — candidates for module extraction.
- Introduce interfaces at infrastructure boundaries incrementally.
- Delete pass-through services that add no policy.
- Add archunit/dependency rules before large refactors so regressions fail CI.
| 1 | HTTP POST /orders |
| 2 | → OrdersController (presentation): validate shape, map DTO |
| 3 | → PlaceOrderHandler (application): begin tx, load entities, call domain |
| 4 | → Order.place() (domain): enforce min items, compute totals |
| 5 | → OrderRepository (infrastructure): INSERT rows |
| 6 | → OutboxPublisher (infrastructure): write event row |
| 7 | ← return order id DTO |
The same flow in a poorly layered system often has SQL and pricing math inside the controller — fast to write, expensive to change safely.
| 1 | [ ] Dependency rule documented and enforced |
| 2 | [ ] Domain free of framework imports (or ADR justifying exceptions) |
| 3 | [ ] Controllers do not access DB session directly |
| 4 | [ ] Business invariants have domain tests |
| 5 | [ ] Physical tiers justified by requirements |
| 6 | [ ] Combined with vertical modules if product is large |
| Requirement | Layered implication |
|---|---|
| Swap UI toolkit | Presentation isolation pays off |
| Complex domain rules | Invest in real domain layer or go hexagonal |
| Simple CRUD admin | Thin layers OK; avoid over-abstraction |
| Separate security zone for DB | Physical tiering may be justified |
Write an ADR when you choose layered as the house style so newcomers know the dependency rule and where business logic must live.
Pure layering organizes by technical role; modular monoliths organize by business capability (with optional layers inside). Large products usually need both: capabilities as the primary cut, layers as the secondary cut.
Layered architecture pairs well with onboarding checklists and code review rubrics ("which layer does this belong in?"). In Agile teams, still deliver vertical slices — implement across layers per story rather than finishing the data layer for all features first (a Waterfall trap inside an Agile calendar).
Community
Get help on Slack, Discord or VIP
Stuck on a guide? Join the community and ask.