Una arquitectura en capas establece límites explícitos para dividir responsabilidades y evitar que las decisiones se mezclen. Este tema profundiza en las tres capas tradicionales (presentación, dominio y persistencia) y describe capas complementarias frecuentes en escenarios empresariales. La clave es preservar la direccionalidad de las dependencias: de la capa superior a la inferior, nunca al revés.
La capa de presentación es el punto de encuentro con las personas usuarias o con clientes externos. Puede materializarse como una aplicación web, una app móvil, una consola o un conjunto de APIs REST. Sus responsabilidades principales son:
Herramientas como Spring Framework ofrecen controladores y anotaciones para implementar esta capa manteniendo un código legible y consistente.
El dominio concentra las reglas esenciales del problema: cómo se generan pedidos, cómo se calculan promociones, qué restricciones aplican al inventario. Esta capa debe permanecer aislada de detalles de interfaz y persistencia para preservar su estabilidad.
El dominio expone interfaces (puertos) que el resto de capas implementa, garantizando independencia respecto de frameworks y bases de datos.
La capa de persistencia resuelve el almacenamiento y la recuperación de información. Puede interactuar con bases de datos relacionales, sistemas NoSQL, colas de mensajes o servicios externos. Sus responsabilidades incluyen:
Frameworks como Jakarta Persistence o controladores JDBC proporcionan utilidades para minimizar el código repetitivo y estandarizar la comunicación con fuentes de datos.
En proyectos amplios es habitual agregar capas intermedias o transversales que complementan a las tres principales:
Estas capas adicionales refuerzan la modularidad y permiten que cada responsabilidad evolucione siguiendo su propio ritmo de cambio.
Centraliza la comunicación con la persona usuaria, aplica validaciones básicas y delega las decisiones complejas al dominio.
package com.example.ventas.presentation;
import com.example.ventas.application.RegistrarPedido;
public class PedidoController {
private final RegistrarPedido registrarPedido;
public PedidoController(RegistrarPedido registrarPedido) {
this.registrarPedido = registrarPedido;
}
public PedidoResponse crear(PedidoRequest request) {
var comando = request.toCommand();
var resultado = registrarPedido.ejecutar(comando);
return PedidoResponse.from(resultado);
}
}
Orquesta casos de uso, protege invariantes y coordina la persistencia mediante contratos definidos por el dominio.
package com.example.ventas.application;
import com.example.ventas.domain.Pedido;
import com.example.ventas.domain.PedidoRepositorio;
public class RegistrarPedido {
private final PedidoRepositorio repositorio;
public RegistrarPedido(PedidoRepositorio repositorio) {
this.repositorio = repositorio;
}
public Pedido ejecutar(RegistrarPedidoCommand command) {
Pedido pedido = Pedido.crear(command.numero(), command.cliente(), command.items());
pedido.validarPoliticas();
repositorio.guardar(pedido);
return pedido;
}
}
Implementa los puertos definidos por el dominio y se conecta con la base de datos garantizando consistencia transaccional.
package com.example.ventas.infrastructure;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import com.example.ventas.domain.Pedido;
import com.example.ventas.domain.PedidoRepositorio;
public class PedidoRepositorioJdbc implements PedidoRepositorio {
private final DataSource dataSource;
public PedidoRepositorioJdbc(DataSource dataSource) {
this.dataSource = dataSource;
}
@Override
public void guardar(Pedido pedido) {
String sql = "INSERT INTO pedidos(numero, cliente, total) VALUES (?, ?, ?)";
try (Connection connection = dataSource.getConnection();
PreparedStatement statement = connection.prepareStatement(sql)) {
statement.setString(1, pedido.numero());
statement.setString(2, pedido.cliente());
statement.setBigDecimal(3, pedido.total());
statement.executeUpdate();
} catch (SQLException ex) {
throw new RuntimeException("No se pudo almacenar el pedido " + pedido.numero(), ex);
}
}
}
El controlador se limita a recibir la solicitud y delegar la lógica al caso de uso, mientras que la capa de infraestructura implementa la persistencia. Esta separación facilita probar cada pieza por separado y cambiar proveedores tecnológicos sin afectar el resto del sistema.
Habiendo identificado las responsabilidades de cada capa, avanzaremos hacia el flujo de comunicación entre ellas. Comprender cómo circulan los datos y qué dependencias están permitidas es esencial para mantener la arquitectura en capas alineada con los objetivos del negocio.