Layered architecture is probably the most widespread architectural style in the world. Almost any enterprise application you have used—a bank, an online store, a policy manager—is internally organized into layers: one for presentation, another for business rules, and another to talk to the database. Its success is due to the fact that it is intuitive, easy to teach, and provides a clear separation of responsibilities. However, applied without judgment, it hides traps such as layers that add nothing (the "sinkhole antipattern") or coupling between tiers. In this lesson we will understand the usual layers, the difference between open and closed layers, and the mistakes worth avoiding.
Contents
- What layered architecture is
- The usual layers: presentation, business, and persistence
- Closed layers vs open layers
- Advantages and drawbacks
- The sinkhole layer antipattern
- Practical end-to-end example
- Common mistakes and tips
- Exercises
- Conclusion
- What layered architecture is
Layered architecture (also called N-Tier or layered architecture) organizes software into horizontal groups of responsibility. Each layer:
- Has a well-defined responsibility.
- Communicates only with the layer immediately below it (in its closed form).
- Hides its internal details from the layers above.
graph TD
P[Presentation Layer\nControllers, views, DTOs]
N[Business Layer\nServices, rules, use cases]
PE[Persistence Layer\nRepositories, DAOs]
BD[(Database)]
P --> N --> PE --> BDIt is worth clarifying a common vocabulary confusion:
- Layer (logical layer): a division of the code by responsibility. Several layers can live in the same process.
- Tier (physical level): a division of the deployment (different machines/processes). The browser, the application server, and the DB server are three tiers.
A monolith can have 3 layers and be deployed on a single tier. They are independent axes.
- The usual layers: presentation, business, and persistence
| Layer | Responsibility | What it DOES contain | What it should NOT contain |
|---|---|---|---|
| Presentation | Interact with the outside world | REST controllers, DTO mapping, format validation | Business rules, SQL |
| Business | Rules and use cases | Services, business validations, transaction orchestration | HTML, raw SQL, HTTP details |
| Persistence | Data access | Repositories, DAOs, object-relational mapping | Business rules, presentation logic |
The central idea: each concept lives in a single layer. If a business rule appears in a controller or in an SQL query, the separation has been broken.
// Presentation Layer: only translates HTTP <-> model
@RestController
@RequestMapping("/polizas")
class PolizaController {
private final ServicioPolizas servicio; // depends on the business layer
PolizaController(ServicioPolizas servicio) { this.servicio = servicio; }
@PostMapping
PolizaResponse crear(@RequestBody @Valid PolizaRequest req) {
// 1) Translates the input DTO into a business command
var id = servicio.contratar(req.toComando());
// 2) Translates the result into an output DTO
return new PolizaResponse(id);
}
}Explanation:
- The controller does not make business decisions: it validates the format of the request (
@Valid), translates it into a command, and delegates toServicioPolizas. - It returns a response DTO belonging to the presentation layer, without leaking internal entities.
- Closed layers vs open layers
This is one of the most important—and most misunderstood—decisions of the layered style.
- Closed layer: a request passing through it cannot skip it. To get from Presentation to Persistence, it must go through Business.
- Open layer: a request can skip it and access the layer below directly.
graph TD
subgraph Closed
A1[Presentation] --> A2[Business] --> A3[Persistence]
end
subgraph With open layer
B1[Presentation] --> B2[Business]
B2 --> B3[Shared services\nOPEN LAYER]
B3 --> B4[Persistence]
B2 -.skips.-> B4
end| Aspect | Closed layers | Open layers |
|---|---|---|
| Isolation between layers | Maximum | Lower |
| Cost of a change | Localized | May propagate |
| Risk of uncontrolled skips | Low | High if abused |
| Typical use | By default, recommended | Shared-service/utility layers |
Recommendation: keep layers closed by default. Only open a layer when there is a strong reason (for example, a cross-cutting utility layer) and document it. Closed layers provide change isolation: if you modify persistence, only the business layer can be affected.
- Advantages and drawbacks
| Advantages | Drawbacks |
|---|---|
| Clear separation of responsibilities that is easy to teach | Can degenerate into many layers that add no value |
| Substitutability: changing the DB affects only persistence | Risk of a sinkhole layer (see section 5) |
| Per-layer testing with mocked dependencies | Vertical coupling still exists |
| Fits naturally into any MVC framework | Rules tend to "leak" into presentation or persistence |
| Low barrier to entry for teams | The domain ends up subordinated to the infrastructure (the DB usually "rules") |
An important nuance regarding later styles (hexagonal, clean): in layered architecture the domain depends downward on persistence. That means the database tends to condition the business model. We will see this solved in later lessons with dependency inversion.
- The sinkhole layer antipattern
The sinkhole layer antipattern (architecture sinkhole anti-pattern) occurs when many requests pass through the layers without those layers doing anything useful: they simply delegate to the layer below. The layer becomes a "sinkhole" that adds code and latency without adding value.
// SYMPTOM: the business layer does nothing, it only forwards
class ServicioClienteSumidero {
private final RepositorioCliente repo;
ServicioClienteSumidero(RepositorioCliente repo) { this.repo = repo; }
// Pure pass-through: no rules, no validation, no orchestration
Cliente buscar(Long id) {
return repo.buscar(id); // so why does this layer exist?
}
}Why is it a problem and when is it NOT?
- It is a problem if most of the system's operations are mere pass-throughs: you are paying the cost of the layer without getting its benefit.
- It is NOT a problem if only some operations are simple while others do contain rules. The usual heuristic is the 80/20 rule: if 80% of requests pass straight through, reconsider the layers; if only 20%, it is acceptable to maintain uniformity.
Possible solutions:
- Open specific layers so that certain simple requests skip directly (with judgment).
- For read-only queries, use a lightweight CQRS pattern that reads directly from the read model.
- Practical end-to-end example
Let's see the three layers collaborating to underwrite a policy, with the business rule in its place.
// --- Business Layer ---
class ServicioPolizas {
private final RepositorioPolizas repo;
private final TarificadorRiesgo tarificador;
ServicioPolizas(RepositorioPolizas repo, TarificadorRiesgo t) {
this.repo = repo; this.tarificador = t;
}
Long contratar(ComandoContratar cmd) {
// Real business rule: reject uninsurable risks
if (!tarificador.esAsegurable(cmd.riesgo())) {
throw new RiesgoNoAsegurableException(cmd.riesgo());
}
var prima = tarificador.calcularPrima(cmd.riesgo());
var poliza = new Poliza(cmd.cliente(), cmd.riesgo(), prima);
return repo.guardar(poliza); // delegates persistence
}
}
// --- Persistence Layer ---
interface RepositorioPolizas {
Long guardar(Poliza poliza);
}Detailed explanation:
ServicioPolizas(business) contains a real decision: it checks whether the risk is insurable and calculates the premium. It is not a sinkhole.RepositorioPolizas(persistence) is defined as an interface: the business layer depends on a contract, not on a concrete JPA or JDBC implementation. This makes it easy to test with a fake repository.- The complete flow is:
PolizaController(presentation) →ServicioPolizas(business) →RepositorioPolizas(persistence) → DB. Closed layers, no skips.
-- The table backing persistence (a detail of the lower layer)
CREATE TABLE polizas (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
cliente VARCHAR(120) NOT NULL,
riesgo VARCHAR(60) NOT NULL,
prima DECIMAL(10,2) NOT NULL
);Note that the SQL only appears in the persistence layer. Neither the controller nor the service knows that behind it there is a relational table.
- Common Mistakes and Tips
- Leaking business rules into presentation. If an
ifdecides something about the domain inside a controller, move it to the service. - Returning persistence entities to the outside world. Expose DTOs; do not leak your internal model through the API.
- Creating empty layers "for symmetry." Do not add a service layer if it does nothing; that is exactly the sinkhole layer.
- Opening all layers "for flexibility." Close by default; open only with justification.
- Confusing layer with tier. Deciding how many layers you have is independent of how many machines you deploy on.
- Tip: number your layers mentally and, for each class, ask yourself "which layer does this responsibility live in?" If you are not sure, the class probably does too much.
- Exercises
Exercise 1. Classify each element into its layer (Presentation, Business, or Persistence):
(a) A @RestController that receives JSON. (b) Calculating the late-payment surcharge. (c) Mapping an entity to its table with JPA. (d) @NotNull validation of the format of an input field.
Exercise 2. Your manager proposes opening all layers "to go faster." Give two arguments against it and explain in which specific case you would indeed open one.
Exercise 3. Detect whether this service is a sinkhole antipattern and propose an improvement:
class ServicioPedidos {
private final RepositorioPedidos repo;
Pedido obtener(Long id) { return repo.buscar(id); }
List<Pedido> listar() { return repo.listarTodos(); }
}Solutions
Solution 1. (a) Presentation. (b) Business. (c) Persistence. (d) Presentation (format validation; business validations would go in the business layer).
Solution 2. Arguments against: (1) change isolation is lost, since presentation could couple directly to persistence; (2) it encourages skipping the business rules, scattering them. Case where I would open one: a cross-cutting utility/shared-services layer (logging, message translation) that encloses no domain rules.
Solution 3. It is a sinkhole antipattern: both methods are pure pass-throughs with no rules. Possible improvement: for these simple reads, apply lightweight CQRS and let presentation query a read model directly, reserving the business layer for operations that do contain rules (creating/modifying orders with validations).
- Conclusion
Layered architecture is the internal organization style par excellence: it separates presentation, business, and persistence, and provides a clear separation of responsibilities that is easy to adopt. We have seen the difference between closed and open layers, their advantages and drawbacks, and the dangerous sinkhole layer antipattern. Its major limitation—that the domain depends downward on the infrastructure—will be precisely what later styles resolve. Before getting there, in the next lesson we will study how these layers are distributed across different machines with Client-Server Architecture, where the axis shifts from the logical (layers) to the physical (tiers).
Application Architecture Course
Module 1: Fundamentals of Application Architecture
- What Is Application Architecture?
- The Role of the Software Architect
- Quality Attributes and Non-Functional Requirements
- Architectural Decisions and Trade-offs
- Architecture Documentation: Views and the C4 Model
Module 2: Design Principles and Tactics
- Coupling, Cohesion and Separation of Concerns
- SOLID Principles Applied to Architecture
- DRY, KISS, YAGNI and Other Design Principles
- Architectural Tactics for Quality Attributes
- Managing Technical Debt
Module 3: Architectural Styles and Patterns
- Monolithic Architecture
- Layered Architecture (N-Tier)
- Client-Server Architecture
- Hexagonal Architecture (Ports and Adapters)
- Clean and Onion Architecture
Module 4: Distributed Architectures and Microservices
- Introduction to Distributed Systems
- Microservices Architecture
- Service Decomposition and Bounded Contexts
- API Gateway, Service Discovery and Inter-Service Communication
- Resilience Patterns: Circuit Breaker, Retry and Bulkhead
- The CAP Theorem and Data Consistency
Module 5: Event-Driven Architectures and Messaging
- Fundamentals of Event-Driven Architecture
- Asynchronous Messaging: Queues and Brokers
- Event Patterns: Event Sourcing and CQRS
- Managing Distributed Transactions: The Saga Pattern
- Real-Time Data Streaming
Module 6: Domain-Driven Design (DDD)
- Core DDD Concepts
- Strategic Design: Bounded Contexts and Ubiquitous Language
- Tactical Design: Entities, Aggregates and Repositories
- Context Mapping
Module 7: Data and Persistence
- Persistence Strategies: SQL vs NoSQL
- Data Access Patterns: Repository, Unit of Work and DAO
- Database per Service and Distributed Data Management
- Caching and Invalidation Strategies
Module 8: Cloud Architecture and Deployment
- Cloud Computing Fundamentals (IaaS, PaaS, SaaS)
- Containers and Orchestration with Docker and Kubernetes
- Serverless Architecture
- Cloud-Native Design Patterns
- Infrastructure as Code (IaC)
Module 9: Quality, Security and Observability
- Scalability: Horizontal vs Vertical and Load Balancing
- High Availability and Fault Tolerance
- Security by Design and Authentication/Authorization
- Observability: Logging, Metrics and Tracing
- Performance and Load Testing
