Layered Architecture
Status: Complete
Category: Architecture
Default enforcement: Advisory
Author: PushBackLog team
Tags
- Topic: architecture, quality
- Skillset: backend, frontend
- Technology: generic
- Stage: execution, review
Summary
Layered (or n-tier) architecture organises a system into horizontal layers — typically Presentation, Application/Domain, and Infrastructure — where each layer has a well-defined responsibility and may only depend on layers below it. This structure creates natural seams for testing, replacement, and scaling of individual concerns.
Rationale
Layers as dependency contracts
Layered architecture is the most common structural pattern for backend systems because it makes explicit what each part of the system is responsible for and what it is allowed to depend on. Without deliberate layering, systems converge on a “big ball of mud”: everything depends on everything, and changing any part requires understanding all parts.
Each layer boundary is a dependency contract. The presentation layer depends on the application layer but not the infrastructure layer. The application layer depends on the domain layer but not on HTTP request objects. The domain layer depends on nothing outside itself. These dependency boundaries enable substitution: swap the database from PostgreSQL to MongoDB by replacing the infrastructure layer, without touching domain or application.
Testability by layer
Layering directly enables the test pyramid. Domain logic is tested with pure unit tests — no infrastructure needed. Application services are tested by injecting test doubles for infrastructure. Infrastructure layer is tested with integration tests against a real (containerised) database. Presentation layer is tested with API-level tests or browser tests.
The evolution to hexagonal / clean architecture
Classic layered architecture (UI → Business → Data) has a well-known weakness: infrastructure (the data layer) sits at the bottom, which means the domain layer often depends on it. Hexagonal architecture (ports and adapters) inverts this: the domain is at the centre, and infrastructure is at the outside, depending on the domain via interfaces. Both are layered models; hexagonal applies DIP to remove the infrastructure dependency on the domain layer.
Guidance
Common layer map
| Layer | Responsibility | Depends on |
|---|---|---|
| Presentation | HTTP/gRPC/GraphQL handling, request/response transformation, auth middleware | Application |
| Application | Use case orchestration, transaction boundaries, command/query dispatch | Domain, Infrastructure interfaces |
| Domain | Business entities, business rules, domain events, aggregates | Nothing outside domain |
| Infrastructure | Database queries, external HTTP calls, file I/O, queue publishing | Domain interfaces |
Dependency rules
- Dependencies flow downward (presentation → application → domain)
- Infrastructure implements interfaces defined by the domain/application layer (DIP)
- Domain objects import nothing from presentation or infrastructure
- Use constructor injection to assemble layers at startup
Directory structure example
src/
presentation/ # Controllers, routes, middleware
orders/
OrderController.ts
application/ # Use cases / services
orders/
CreateOrderService.ts
GetOrderService.ts
domain/ # Entities, value objects, interfaces
orders/
Order.ts
IOrderRepository.ts
infrastructure/ # Concrete implementations
database/
PostgresOrderRepository.ts
email/
SendGridEmailSender.ts
Examples
Flow through layers for “Create Order”
HTTP POST /orders
↓ OrderController (presentation)
- validates request shape
- calls CreateOrderService with a Command object
↓ CreateOrderService (application)
- loads products from IProductRepository
- creates Order aggregate (domain)
- saves via IOrderRepository
- publishes OrderCreated event
↓ Order (domain)
- applies business rules (discount cap, inventory check)
- emits domain events
↓ PostgresOrderRepository (infrastructure)
- writes to database
Each layer has a single job. Any layer can be tested by replacing adjacent layers with fakes.
Anti-patterns
1. Layer skipping
A controller calling the repository directly, bypassing the application and domain layers. This means business rules can only be enforced at the HTTP boundary, and the shortcut accumulates until the architecture is meaningless.
2. Anemic domain model
Domain objects that are pure data holders (getters/setters) with all logic in service classes. This is technically layered, but the domain layer has no content — business rules are scattered across service classes instead of being encapsulated in domain objects.
3. Infrastructure types leaking into domain
A domain entity that imports pg.Row or mongoose.Document. When the domain is aware of the database technology, the infrastructure cannot be swapped without domain changes.
4. God service
A single OrderService with 30 methods that spans all use cases and all concerns. Should be split into focused use-case handlers or application services, each with a single responsibility.
Related practices
Part of the PushBackLog Best Practices Library. Suggest improvements →