Una arquitectura en capas cobra sentido cuando definimos cómo se comunican sus componentes. Las interacciones dictan la estabilidad del sistema tanto como la división de responsabilidades. En este tema describimos el flujo de mensajes, las reglas de dependencia y un recorrido completo desde la interfaz de usuario hasta la base de datos.
En un sistema N-Tier cada capa actúa como proveedor de servicios para la capa inmediatamente superior. La comunicación se realiza mediante interfaces claras que exponen datos de entrada y salida. En términos generales, el recorrido sigue estos pasos:
El flujo puede extenderse a capas adicionales, como servicios externos o mensajería, pero siempre se mantiene el principio de escalones: cada mensaje avanza hacia abajo y retorna por la misma ruta.
Un principio esencial es que las dependencias solo se permiten en dirección descendente. La capa de dominio no conoce detalles de presentación y la de persistencia no invoca servicios del dominio directamente sin que medien interfaces. Este orden facilita el reemplazo de componentes y evita ciclos peligrosos.
Respetar la dirección descendente evita que un cambio en la interfaz rompa la persistencia o que la base de datos se convierta en un atajo para saltar reglas de negocio.
El siguiente diagrama textual describe el recorrido de un pago desde que una persona confirma la operación en la interfaz hasta que queda persistido:
ConfirmarPago.Todo el viaje respeta el sentido descendente de las dependencias. El siguiente esquema destaca cada capa con su código representativo y las flechas del flujo de solicitud y respuesta.
Recibe la petición, valida datos de entrada y delega al caso de uso.
package com.example.ventas.presentation;
import com.example.ventas.application.ConfirmarPago;
public class PagoRestController {
private final ConfirmarPago confirmarPago;
public PagoRestController(ConfirmarPago confirmarPago) {
this.confirmarPago = confirmarPago;
}
public PagoResponse confirmar(PagoRequest request) {
var comando = request.toCommand();
var recibo = confirmarPago.ejecutar(comando);
return PagoResponse.from(recibo);
}
}
Procesa el comando, aplica reglas de negocio y coordina la persistencia del pago.
package com.example.ventas.application;
import com.example.ventas.domain.Pago;
import com.example.ventas.domain.PagoRepositorio;
import com.example.ventas.domain.ServicioFraude;
public class ConfirmarPago {
private final PagoRepositorio repositorio;
private final ServicioFraude servicioFraude;
public ConfirmarPago(PagoRepositorio repositorio, ServicioFraude servicioFraude) {
this.repositorio = repositorio;
this.servicioFraude = servicioFraude;
}
public Pago ejecutar(ConfirmarPagoCommand command) {
servicioFraude.validar(command);
Pago pago = Pago.registrar(command.ordenId(), command.monto());
repositorio.guardar(pago);
return pago;
}
}
Concreta la operación contra la base de datos y confirma la transacción.
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.Pago;
import com.example.ventas.domain.PagoRepositorio;
public class PagoRepositorioJdbc implements PagoRepositorio {
private final DataSource dataSource;
public PagoRepositorioJdbc(DataSource dataSource) {
this.dataSource = dataSource;
}
@Override
public void guardar(Pago pago) {
String sql = "INSERT INTO pagos(orden_id, monto, confirmado_en) VALUES (?, ?, ?)";
try (Connection connection = dataSource.getConnection();
PreparedStatement statement = connection.prepareStatement(sql)) {
statement.setString(1, pago.ordenId());
statement.setBigDecimal(2, pago.monto());
statement.setTimestamp(3, pago.confirmadoEn());
statement.executeUpdate();
} catch (SQLException ex) {
throw new RuntimeException("No se pudo confirmar el pago de la orden " + pago.ordenId(), ex);
}
}
}
La capa de presentación transforma los datos de entrada en comandos, el dominio ejecuta las reglas y la capa de persistencia contacta a la base de datos. No existe una referencia inversa desde el repositorio hacia el controlador, garantizando la dirección del flujo.
Dominar el flujo de comunicación permite identificar cuellos de botella y planificar optimizaciones. En el siguiente tema construiremos un ejemplo completo de aplicación en tres capas, incluyendo decisiones de empaquetado y pruebas coordinadas.