El Principio de Responsabilidad Única nos dice que clases y funciones deben tener una responsabilidad clara y específica para conseguir código de mayor calidad. Pero podemos ir un paso más allá aplicando este mismo criterio para organizar una aplicación completa, agrupando componentes según su responsabilidad y definiendo cómo se comunican entre sí.

Este enfoque permite estructurar el sistema de forma que sea más robusto, mantenible y fácil de evolucionar.

En qué consiste

La arquitectura por capas consiste en organizar una aplicación en módulos que comparten una responsabilidad clara, estableciendo además una dirección definida en sus dependencias. Cada capa se comunica únicamente con las capas adyacentes.

Los objetivos que persigue son:

  • Reducir el acoplamiento.
  • Facilitar el mantenimiento.
  • Permitir evolución tecnológica sin afectar al núcleo.
  • Mejorar la testabilidad.
  • Aislar la lógica de negocio.

Estructura básica

Una división mínima podría representarse así:

[    Entrada     ] - Capa de entrada/transporte/salida (HTTP, API...)
[    Negocio     ] - Lógica de negocio
[  Persistencia  ] - Acceso a datos
[  Base de datos ]
  • La capa de entrada recibe peticiones y devuelve respuestas.
  • La capa de negocio contiene la lógica que implementa las reglas de la aplicación.
  • La capa de persistencia se encarga del acceso a los datos.

Cada capa tiene responsabilidades claras y solo conoce la capa inmediatamente inferior. De esta forma se evita que detalles de infraestructura o transporte se mezclen con la lógica de negocio.

Por ejemplo:

  • Validar que una dirección de correo tiene un formato correcto pertenece a la capa de entrada, ya que depende de cómo llegan los datos (JSON, formulario, etc.).
  • Comprobar si ese correo pertenece a un cliente registrado es una regla de negocio, por lo que debe implementarse en la capa de negocio.

De la misma forma, el cálculo del importe total de un pedido debería hacerse en la capa de negocio. Si esta lógica se implementara en la base de datos, cambiar de sistema gestor implicaría rehacer parte de la lógica de la aplicación.

Cómo se rompe la arquitectura por capas

Estructurar una aplicación por capas no consiste únicamente en separar el código en carpetas o crear clases llamadas Controller, Service o Repository. Lo importante es respetar las responsabilidades y la dirección de las dependencias.

Un ejemplo de una API de gestión de pedidos donde aparentemente existen las tres capas puede ser el siguiente:

Controlador

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@RestController
@RequestMapping("/orders")
public class OrderController {

    @Autowired
    private OrderService orderService;

    @Autowired
    private CouponRepository couponRepository;

    @PostMapping
    public ResponseEntity<OrderResponseDto> createOrder(@RequestBody OrderRequestDto request) {

        Coupon coupon = null;

        if (request.getCouponCode() != null && !request.getCouponCode().isBlank()) {
            coupon = couponRepository.findByCode(request.getCouponCode())
                    .orElseThrow(() -> new RuntimeException("Cupón no encontrado"));
        }

        Order order = orderService.createOrder(request, coupon);

        return ResponseEntity.ok(mapper.toResponse(order));
    }
}

Servicio

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
@Service
public class OrderService {

    @Autowired
    private UserRepository userRepository;

    @Autowired
    private ProductRepository productRepository;

    @Autowired
    private OrderRepository orderRepository;

    public Order createOrder(OrderRequestDto request, Coupon coupon) {

        User user = userRepository.findById(request.getUserId())
                .orElseThrow(() -> new RuntimeException("Usuario no encontrado"));

        BigDecimal totalBeforeDiscount = BigDecimal.ZERO;

        for (OrderItemRequestDto item : request.getItems()) {

            Product product = productRepository.findById(item.getProductId())
                    .orElseThrow(() -> new RuntimeException("Producto no encontrado"));

            BigDecimal lineTotal = product.getPrice()
                    .multiply(BigDecimal.valueOf(item.getQuantity()));

            totalBeforeDiscount = totalBeforeDiscount.add(lineTotal);
        }

        BigDecimal discountApplied = BigDecimal.ZERO;

        if (coupon != null && coupon.isActive()) {
            discountApplied = totalBeforeDiscount
                    .multiply(coupon.getPercentage())
                    .divide(BigDecimal.valueOf(100));
        }

        Order order = new Order();
        order.setUser(user);
        order.setCreatedAt(LocalDateTime.now());
        order.setTotalBeforeDiscount(totalBeforeDiscount);
        order.setDiscountApplied(discountApplied);
        order.setTotalFinal(totalBeforeDiscount.subtract(discountApplied));

        return orderRepository.save(order);
    }
}

Repositorio

1
2
3
4
@Repository
    public interface CouponRepository extends JpaRepository<Coupon, Long> {
    Optional<Coupon> findByCode(String code);
}

A primera vista parece una aplicación bien estructurada: existen controladores, servicios y repositorios. Sin embargo, la arquitectura por capas no se está respetando.

Qué está mal aquí

1. El controlador accede directamente al repositorio

El Controller consulta CouponRepository por su cuenta. Esto rompe la dirección natural de comunicación: Controller → Service → Repository; y hace que la capa de entrada dependa directamente de la persistencia.

2. La lógica de negocio queda repartida

El controlador decide si hay que buscar el cupón y pasa el resultado al servicio. Parte del caso de uso empieza en el controlador y continúa en el servicio.

3. El servicio no controla completamente el caso de uso

El método createOrder depende de que el controlador haya resuelto previamente cierta información, por lo que el servicio deja de representar el flujo completo de creación de pedidos.

4. El servicio recibe estructuras de transporte

El servicio recibe OrderRequestDto, que pertenece a la capa de entrada. Esto mezcla el modelo de transporte con la lógica de negocio.

Problemas que genera

Este tipo de diseño puede funcionar, pero genera varios problemas:

  • Reglas repartidas: cambios en la lógica obligan a modificar varias capas.
  • Menor reutilización: otros puntos de entrada (batch, eventos, etc.) necesitarían replicar parte de la lógica.
  • Servicios menos autónomos: el servicio ya no representa el caso de uso completo.
  • Controladores demasiado complejos: empiezan a conocer detalles de persistencia y reglas de negocio.

Versión correcta

En una arquitectura por capas el controlador debería limitarse a recibir la petición y delegar el caso de uso en la capa de negocio.

1
2
3
4
5
6
@PostMapping
public ResponseEntity<OrderResponseDto> createOrder(@RequestBody OrderRequestDto request) {
    CreateOrderDTO orderDto = mapper.toCreateOrderDto(request);
    Order order = orderService.createOrder(orderDto);
    return ResponseEntity.ok(mapper.toResponse(order));
}

El servicio se encarga entonces de todo el flujo del caso de uso:

1
2
3
4
5
6
7
8
public Order createOrder(CreateOrderDTO orderDto) {
    Discount discount = discountService.calculateDiscount(orderDto);
    // buscar usuario
    // buscar productos
    // validar cupón
    // calcular totales
    // guardar pedido
}

De esta forma:

  • el Controller gestiona el transporte (HTTP, DTOs, respuesta),
  • el Service implementa la lógica de negocio,
  • los Repositories se encargan únicamente del acceso a datos.

Conclusiones

Una arquitectura por capas no se rompe solo cuando una clase hace demasiado, sino también cuando las responsabilidades parecen separadas pero el flujo real del caso de uso queda repartido entre capas que no deberían conocer esos detalles.

Tener clases llamadas Controller, Service y Repository no garantiza una arquitectura por capas. Lo que realmente la define es respetar las responsabilidades de cada capa y mantener una dirección clara en las dependencias.