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

  1. What layered architecture is
  2. The usual layers: presentation, business, and persistence
  3. Closed layers vs open layers
  4. Advantages and drawbacks
  5. The sinkhole layer antipattern
  6. Practical end-to-end example
  7. Common mistakes and tips
  8. Exercises
  9. Conclusion

  1. 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 --> BD

It 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.

  1. 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 to ServicioPolizas.
  • It returns a response DTO belonging to the presentation layer, without leaking internal entities.

  1. 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.

  1. 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.

  1. 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.

  1. 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.

  1. Common Mistakes and Tips

  • Leaking business rules into presentation. If an if decides 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.

  1. 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).

  1. 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

Module 2: Design Principles and Tactics

Module 3: Architectural Styles and Patterns

Module 4: Distributed Architectures and Microservices

Module 5: Event-Driven Architectures and Messaging

Module 6: Domain-Driven Design (DDD)

Module 7: Data and Persistence

Module 8: Cloud Architecture and Deployment

Module 9: Quality, Security and Observability

Module 10: Evolution, Governance and Case Studies

© Copyright 2026. All rights reserved