Layered Architecture
Organizes software into horizontal layers with well-defined responsibilities, where each layer depends only on the layers below it — creating a single direction of dependency and isolating business logic from infrastructure.
Intent
Split the system into cohesive horizontal layers, where each layer has a clear responsibility and knows only the layers immediately below it. This creates a unidirectional dependency rule: upper layers depend on lower layers, never the other way around.
Layered Architecture (also called N-tier Architecture) is one of the oldest and most widespread architectural patterns. It's the foundation on which frameworks like Laravel, Spring, Django and ASP.NET build their code-organization conventions. It's not a GoF pattern — it's a larger-grained architectural pattern that organizes how groups of classes relate to one another.
Problem
In applications without layer separation, it's common to find:
- Controllers that query the database directly.
- Domain entities that know details of HTTP serialization or persistence.
- Business rules scattered across controllers, models and even views.
- Tests that need a real database to verify a simple business rule.
The result is high fragility: a database change breaks the controller; an HTTP contract change breaks the domain. Layered Architecture solves this by making explicit where each kind of code lives and what it's allowed to depend on.
Structure
The four typical layers
┌──────────────────────────────────────────────────────────┐
│ Presentation Layer │
│ HTTP Controllers, CLI handlers, JSON serialization, │
│ templates, input validation (format/type) │
└──────────────────────────┬───────────────────────────────┘
│ depends on
▼
┌──────────────────────────────────────────────────────────┐
│ Application Layer (Service) │
│ Use cases, domain orchestration, transactions, │
│ per-use-case authorization, input/output DTOs │
└──────────────────────────┬───────────────────────────────┘
│ depends on
▼
┌──────────────────────────────────────────────────────────┐
│ Domain Layer │
│ Entities, Value Objects, business rules, │
│ repository interfaces, domain events │
│ (pure core — imports nothing from the outer layers) │
└──────────────────────────┬───────────────────────────────┘
│
┌──────────┴────────────────────────────────────────────┐
│ WITHOUT DIP (pure classic layering): │
│ Domain → Infrastructure │
│ (depends directly on the concrete implementation) │
│ │
│ WITH DIP + Repository (recommended practice): │
│ Infrastructure → Domain │
│ (Infra implements the interface the Domain defines; │
│ the Domain does NOT know the concrete Infra) │
└───────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────┐
│ Infrastructure / Persistence Layer │
│ Repository implementations (SQL, NoSQL, API), │
│ external gateways, queues, cache, email sending │
└──────────────────────────────────────────────────────────┘
Arrow direction: whoever points depends on whoever is pointed to.
With DIP, Infrastructure points to the Domain (implements its interfaces);
the Domain remains free of external dependencies — the most stable form.
Strict vs Relaxed Layering
In strict layering, each layer can only call the layer immediately below it. The Presentation Layer can't call the Domain Layer directly — it has to go through the Application Layer. This guarantees maximum isolation, but can add unnecessary pass-through code in simple cases.
In relaxed layering, a layer can call any layer below it, not just the immediate one. The Presentation Layer can call the Domain Layer directly for simple read operations. More pragmatic, but requires discipline to avoid shortcuts that erode isolation.
Strict layering:
Presentation → Application → Domain → Infrastructure
(each layer only sees the one immediately below it)
Relaxed layering:
Presentation → Application
Presentation → Domain (allowed for simple reads)
Application → Domain
Application → Infrastructure (via Domain interfaces)
Domain → (nothing external)
How it works — request flow
Following the direction of the dependencies, a typical request flows from top to bottom, and the response goes back up:
- Presentation: the Controller receives the HTTP request, validates the format of the parameters (types, required fields) and builds a command/query DTO for the Application layer.
- Application: the Application Service (or Use Case) receives the DTO, applies per-use-case authorization rules, coordinates the necessary domain operations and delegates persistence to the repository interfaces.
- Domain: entities and domain services apply the business rules. This is where the invariants live — things that must always hold true, regardless of any framework or database.
- Infrastructure: concrete repository implementations run queries; external-service adapters make HTTP calls; the result flows back up the chain.
The golden rule: dependencies only point downward. The Domain never imports anything from the Application or the Presentation. The Infrastructure implements interfaces defined by the Domain — but the Domain doesn't know the Infrastructure.
When to use
- Business applications with non-trivial domain logic: systems with complex authorization rules, workflows, business calculations. Layer separation ensures the domain logic stays testable and isolated.
- Larger teams with split responsibilities: the layer separation creates natural boundaries — one team handles the domain layer, another the infrastructure, another the presentation — with clear contracts between them.
- When there are multiple entry points: a REST API, a CLI interface and a queue worker can share the same Application and Domain layers — only the Presentation changes.
- To make infrastructure replacement easier: switching databases, email providers or queue systems should impact only the Infrastructure layer, without touching the Domain.
When to avoid
- Simple CRUDs without real domain logic: applications that are essentially create/read/update/delete forms without complex rules don't justify four layers. A simpler structure — or even direct MVC — is more appropriate.
- Very small, focused microservices: a service that does just one thing (e.g., sending notifications) rarely needs separate domain layers. The architectural overhead doesn't pay off.
- Prototyping and MVPs: the layered structure has an initial organizational cost. In discovery phases, the focus should be on validating the idea — the architecture can evolve afterward.
Pros and cons
Pros
- Clear and verifiable separation of responsibilities — you can audit whether the dependency rule is being respected.
- Domain fully testable without a database, without HTTP, without a framework.
- Makes replacing infrastructure easier without impacting the domain.
- Well understood by most teams — has extensive documentation and examples in every major language.
- Compatible with multiple entry points (HTTP, CLI, workers) sharing the same core.
Cons
- Structuring overhead for simple systems — too many layers for a basic CRUD.
- Risk of "lasagna effect": proliferation of layers without a clear purpose (a "helpers" layer, a "utilities" layer, sub-layers of sub-layers).
- The dependency rule isn't enforced by the compiler — it requires team discipline or static analysis tools (e.g., ArchUnit, Deptrac).
- Pass-through code: objects that exist only to carry data between layers without real transformation increase code volume without value.
Common pitfalls
1. Lasagna effect — too many layers without purpose
Every new layer added should have a clear justification: what kind of responsibility does it encapsulate that doesn't belong to adjacent layers? Layers like "Utils", "Helpers", "Common" or "Shared" that accumulate everything nobody knows where to put are a sign the structure isn't being thought through carefully. Prefer few layers with sharp boundaries over many layers with vague boundaries.
2. Leakage between layers
The most common type of violation: Domain entities with database annotations (JPA, Doctrine, Eloquent casts that know the schema), Controllers that directly manipulate database objects (ActiveRecord in the Controller), or the Domain layer importing types from an HTTP framework. These leaks gradually destroy the isolation — the initial violation seems harmless, but it sets the precedent for the next ones.
Watch out for Active Record: ORMs with the Active Record pattern (Eloquent in Laravel, ActiveRecord in Rails) merge the domain entity with the persistence infrastructure. They're extremely productive, but make strict separation between Domain and Infrastructure harder. In projects where that separation is critical, prefer the Repository pattern with explicit mapping.
3. Anemic domain
When business rules that should live in the Domain migrate to the Application Services, the Domain becomes just a set of data classes with no behavior — the Anemic Domain Model described by Martin Fowler. The result is that every rule ends up in services, which become hard to compose and test in isolation. Keep the core invariants and behaviors in the Domain's entities and Value Objects.
4. Not verifying the dependency rule automatically
In large teams, the rule "upper layers can't be imported by lower layers" gets violated bit by bit, especially by less experienced developers or under time pressure. Use dependency-analysis tools (ArchUnit for Java, Deptrac for PHP, modules with import restrictions in TypeScript/NestJS) to turn the violation into a build error, not just a bad practice.
Related architectures and patterns
MVC can be seen as a specialization of Layered Architecture focused on the separation between the presentation layer (View + Controller) and the data/business layer (Model). Layered Architecture defines what happens inside the Model — how it subdivides into Application, Domain and Infrastructure.
Hexagonal Architecture shares the same concern of isolating the domain from infrastructure, but explicitly inverts the dependencies: instead of "infrastructure sits below the domain," it says "infrastructure implements interfaces defined by the domain." The result is the same isolation, but with a different mental model — the domain at the center, adapters at the edges.
Clean Architecture and Onion Architecture are evolutions of Layered Architecture with an explicit emphasis on Dependency Inversion: the inner layers (domain) define interfaces; the outer layers (infrastructure) implement those interfaces. The dependency direction is explicitly inverted compared to classic Layered Architecture, where the Infrastructure sits "below" and is naturally a dependency of the upper layers.