La arquitectura hexagonal abraza el Principio de Inversión de Dependencias (DIP) del acrónimo SOLID para garantizar que las decisiones del dominio sean independientes de la tecnología. Seguir este principio significa que los módulos de alto nivel (casos de uso, reglas de negocio) no dependen de detalles de bajo nivel (frameworks, bases de datos, controladores) sino de abstracciones. De esa manera la aplicación puede evolucionar sin reescribir su corazón cada vez que aparece una nueva herramienta.
Este tema detalla cómo se aplica el DIP en el diseño hexagonal, cómo los puertos juegan el rol de abstracciones compartidas, por qué el dominio queda protegido de frameworks y cuáles son los beneficios concretos para las pruebas y el mantenimiento continuo.
El DIP establece dos reglas: los módulos de alto nivel no deben depender de módulos de bajo nivel y ambos deben depender de abstracciones; además, las abstracciones no deben depender de detalles, sino que los detalles deben depender de las abstracciones. En una aplicación hexagonal esto se traduce en que:
Al invertir la dependencia, cambia el sentido tradicional: en lugar de que el dominio se adapte al framework de turno, son los detalles quienes se alinean al modelo de negocio.
Los puertos son la materialización de esas abstracciones. Definen contratos claros que la infraestructura debe cumplir para colaborar con la lógica de negocio. Al ubicarlos en el dominio o en la capa de aplicación según su tipo, obligan a que cualquier adaptador se ajuste al lenguaje del negocio.
Esta abstracción trae consigo varias consecuencias positivas:
Cuando se descubre que un adaptador necesita un dato adicional, se negocia a nivel del puerto; así se evita que cada detalle técnico modifique libremente el dominio.
Consideremos un contexto de gestión de inventario. El caso de uso ReponerStock debe registrar un movimiento y avisar cuando el stock queda por debajo de un umbral. El dominio define interfaces y clases puras, sin anotaciones ni referencias a frameworks.
package com.example.inventario.dominio;
import java.time.LocalDateTime;
public class MovimientoStock {
private final String sku;
private final int cantidad;
private final LocalDateTime fecha;
public MovimientoStock(String sku, int cantidad, LocalDateTime fecha) {
if (sku == null || sku.isBlank()) {
throw new IllegalArgumentException("El SKU es obligatorio");
}
if (cantidad == 0) {
throw new IllegalArgumentException("La cantidad no puede ser cero");
}
this.sku = sku;
this.cantidad = cantidad;
this.fecha = fecha != null ? fecha : LocalDateTime.now();
}
public String sku() {
return sku;
}
public int cantidad() {
return cantidad;
}
public LocalDateTime fecha() {
return fecha;
}
}
package com.example.inventario.dominio;
public interface PuertoInventario {
void registrarMovimiento(MovimientoStock movimiento);
int stockActual(String sku);
}
package com.example.inventario.dominio;
public interface PuertoAlertas {
void notificarBajoStock(String sku, int stockDisponible);
}
package com.example.inventario.aplicacion;
import com.example.inventario.dominio.MovimientoStock;
import com.example.inventario.dominio.PuertoAlertas;
import com.example.inventario.dominio.PuertoInventario;
public class ReponerStock {
private final PuertoInventario puertoInventario;
private final PuertoAlertas puertoAlertas;
public ReponerStock(PuertoInventario puertoInventario, PuertoAlertas puertoAlertas) {
this.puertoInventario = puertoInventario;
this.puertoAlertas = puertoAlertas;
}
public void ejecutar(String sku, int cantidad) {
MovimientoStock movimiento = new MovimientoStock(sku, cantidad, java.time.LocalDateTime.now());
puertoInventario.registrarMovimiento(movimiento);
int disponible = puertoInventario.stockActual(sku);
if (disponible <= 5) {
puertoAlertas.notificarBajoStock(sku, disponible);
}
}
}
La clase ReponerStock solo conoce las abstracciones PuertoInventario y PuertoAlertas. No importa si el stock se persiste en una base relacional, en una hoja de cálculo o en un servicio externo; la infraestructura debe adaptarse al contrato. Tampoco hace referencia a anotaciones de inyección, sesiones de Hibernate ni repositorios específicos.
Los adaptadores concretos se implementan en la infraestructura y dependen del dominio para conocer los puertos. A modo de ejemplo, un adaptador basado en Spring Data JPA y otro que envía alertas por correo podrían lucir de la siguiente manera:
package com.example.inventario.infraestructura.salida.jpa;
import org.springframework.stereotype.Repository;
import com.example.inventario.dominio.MovimientoStock;
import com.example.inventario.dominio.PuertoInventario;
@Repository
class InventarioJpaAdapter implements PuertoInventario {
private final MovimientoJpaRepository repository;
InventarioJpaAdapter(MovimientoJpaRepository repository) {
this.repository = repository;
}
@Override
public void registrarMovimiento(MovimientoStock movimiento) {
repository.save(MovimientoEntity.desdeDominio(movimiento));
}
@Override
public int stockActual(String sku) {
return repository.sumarCantidadPorSku(sku);
}
}
package com.example.inventario.infraestructura.salida.notificaciones;
import com.example.inventario.dominio.PuertoAlertas;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.stereotype.Component;
@Component
class AlertaCorreoAdapter implements PuertoAlertas {
private final JavaMailSender mailSender;
AlertaCorreoAdapter(JavaMailSender mailSender) {
this.mailSender = mailSender;
}
@Override
public void notificarBajoStock(String sku, int stockDisponible) {
SimpleMailMessage mensaje = new SimpleMailMessage();
mensaje.setSubject("Stock crítico para " + sku);
mensaje.setText("Disponible: " + stockDisponible + " unidades.");
mensaje.setTo("compras@example.com");
mailSender.send(mensaje);
}
}
Cada adaptador depende del dominio porque implementa interfaces declaradas allí. El dominio, en cambio, nunca depende de JavaMailSender, interfaces JPA ni de anotaciones de Spring. Esto evidencia cómo la inversión de dependencias se respeta en la práctica.
Separar el dominio de los detalles técnicos trae ventajas tangibles:
Al combinar el DIP con la estructura hexagonal, los sistemas evolucionan de forma sostenible: el dominio sigue siendo el punto de verdad, mientras que la infraestructura puede renovarse tantas veces como sea necesario sin afectar la experiencia del negocio ni los ciclos de prueba.