Adoptar la arquitectura hexagonal implica más que conocer sus principios: requiere plasmar una estructura concreta que favorezca la independencia del dominio y la sustitución de tecnologías. Un proyecto bien organizado facilita el recambio de adaptadores, clarifica qué código pertenece al corazón del negocio y dirige las dependencias en un solo sentido.
En este tema analizamos la disposición habitual de capas, la organización de paquetes en distintos lenguajes, la administración de dependencias y la separación entre las entradas y salidas del sistema. Cada sección incluye recomendaciones prácticas y un ejemplo en Java para ilustrar los conceptos.
La arquitectura hexagonal no elimina el concepto de capas, sino que redefine su finalidad. Las capas dejan de representar niveles técnicos (presentación, negocio, datos) y pasan a expresar un orden de independencia. El dominio se mantiene puro, la aplicación orquesta casos de uso y la infraestructura provee adaptadores concretos.
Un recorrido habitual del flujo es: un adaptador de entrada recibe una solicitud, invoca un caso de uso en la capa de aplicación, el dominio procesa la lógica y finalmente se delega en adaptadores de salida para persistir o comunicar resultados. Dado que cada capa depende hacia adentro, la inversión de control se logra mediante interfaces y la inyección de dependencias.
package com.example.pagos.dominio;
import java.math.BigDecimal;
public class Pago {
private final String referencia;
private final BigDecimal monto;
public Pago(String referencia, BigDecimal monto) {
if (referencia == null || referencia.isBlank()) {
throw new IllegalArgumentException("La referencia es obligatoria");
}
if (monto == null || monto.signum() <= 0) {
throw new IllegalArgumentException("El monto debe ser positivo");
}
this.referencia = referencia;
this.monto = monto;
}
public String referencia() {
return referencia;
}
public BigDecimal monto() {
return monto;
}
}
package com.example.pagos.aplicacion;
import com.example.pagos.dominio.Pago;
import com.example.pagos.dominio.PuertoPagos;
public class ProcesarPago {
private final PuertoPagos puertoPagos;
public ProcesarPago(PuertoPagos puertoPagos) {
this.puertoPagos = puertoPagos;
}
public void ejecutar(String referencia, double monto) {
Pago pago = new Pago(referencia, java.math.BigDecimal.valueOf(monto));
puertoPagos.registrar(pago);
}
}
package com.example.pagos.dominio;
public interface PuertoPagos {
void registrar(Pago pago);
}
La clase ProcesarPago no conoce cómo se registra un pago; simplemente invoca al puerto PuertoPagos. Dicho puerto se implementa en la infraestructura, permitiendo que la aplicación opere con independencia de la tecnología elegida para almacenar o propagar pagos.
Una estructura coherente de paquetes hace visible la arquitectura. En Java es común separar el código de cada capa en paquetes hermanos dentro de un proyecto único o en módulos independientes de Maven o Gradle. En otros lenguajes la idea es equivalente: agrupar archivos según pertenezcan al dominio, la aplicación o la infraestructura.
src/main/java/com/example/pagos/
dominio/
Pago.java
PuertoPagos.java
aplicacion/
ProcesarPago.java
ConsultarPago.java
infraestructura/
entrada/
RestPagoController.java
salida/
PagoRepositoryJpa.java
PagoEventPublisher.java
Aunque el árbol anterior esté contenido en un solo artefacto, la estructura de paquetes separa responsabilidades. Algunos equipos prefieren un enfoque multimódulo: un artefacto pagos-dominio, otro pagos-aplicacion y un tercero pagos-infraestructura. Así pueden versionar cada pieza por separado y reforzar las dependencias direccionales mediante el sistema de compilación.
En Python, la organización puede reflejarse con paquetes equivalentes (dominio, aplicacion, infraestructura) dentro de un proyecto FastAPI o Django, mientras que en Node.js es habitual dividir carpetas con la misma filosofía. Lo importante es que el nombre del paquete comunique si el código es parte del corazón del negocio o de un adaptador específico.
Cuando se usan frameworks como Spring Boot, conviene evitar colocar anotaciones del framework en clases del dominio. Las anotaciones y configuraciones deben residir en la infraestructura, dejando a los paquetes internos libres de dependencias externas.
El principio central de la arquitectura hexagonal es que las dependencias se orienten hacia el dominio. Esto significa que la aplicación conoce al dominio, pero el dominio ignora la aplicación; la infraestructura conoce a ambos, pero ninguno de los dos conoce a la infraestructura. Para sostener esta regla:
En proyectos multimódulo de Maven, el pom.xml del módulo de aplicación declara dependencia hacia el dominio, mientras que el de infraestructura depende de ambos. El dominio no declara dependencias a módulos internos.
<!-- pagos-aplicacion/pom.xml -->
<dependencies>
<dependency>
<groupId>com.example</groupId>
<artifactId>pagos-dominio</artifactId>
<version>1.0.0</version>
</dependency>
</dependencies>
<!-- pagos-infraestructura/pom.xml -->
<dependencies>
<dependency>
<groupId>com.example</groupId>
<artifactId>pagos-dominio</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>com.example</groupId>
<artifactId>pagos-aplicacion</artifactId>
<version>1.0.0</version>
</dependency>
</dependencies>
Esta configuración impide que el dominio importe clases de la infraestructura. Si un desarrollador intenta hacerlo, el compilador fallará. Además, limita la cantidad de puntos de acoplamiento: los adaptadores sólo conocen las interfaces, y el dominio ignora si la implementación utiliza JPA, JDBC o mensajería basada en AMQP.
Un sistema hexagonal distingue dos tipos de adaptadores: los que introducen información al dominio (entrada) y los que interactúan con el exterior para almacenar o comunicar datos (salida). Separarlos reduce el riesgo de mezclar responsabilidades y favorece la sustitución selectiva.
Los adaptadores de entrada pueden ser controladores REST, listeners de colas o interfaces de línea de comandos. Todos ellos traducen la petición externa a un comando o DTO propio del dominio y lo remiten al puerto de entrada. Por su parte, los adaptadores de salida implementan puertos que definen necesidades del dominio, como guardar un pago o publicar un evento.
package com.example.pagos.infraestructura.entrada;
import com.example.pagos.aplicacion.ProcesarPago;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/pagos")
class RestPagoController {
private final ProcesarPago procesarPago;
RestPagoController(ProcesarPago procesarPago) {
this.procesarPago = procesarPago;
}
@PostMapping
ResponseEntity<Void> registrar(@RequestBody PagoRequest request) {
procesarPago.ejecutar(request.referencia(), request.monto());
return ResponseEntity.accepted().build();
}
record PagoRequest(String referencia, double monto) {}
}
package com.example.pagos.infraestructura.salida;
import com.example.pagos.dominio.Pago;
import com.example.pagos.dominio.PuertoPagos;
import org.springframework.stereotype.Repository;
@Repository
class PagoRepositoryJpa implements PuertoPagos {
private final PagoJpaRepository repository;
PagoRepositoryJpa(PagoJpaRepository repository) {
this.repository = repository;
}
@Override
public void registrar(Pago pago) {
repository.save(PagoEntity.desdeDominio(pago));
}
}
El controlador REST (entrada) y el repositorio JPA (salida) viven en carpetas separadas. Cada adaptador se enfoca en traducir la petición externa al lenguaje del dominio o viceversa. Esta organización evita que la infraestructura trate de orquestar lógica de negocio y mantiene el núcleo de la aplicación pequeño y testeable.
Siguiendo esta disciplina, es posible reemplazar el adaptador REST por uno de mensajería o sustituir el repositorio JPA por un adaptador que invoque un servicio externo, sin que el dominio ni los casos de uso sufran modificaciones.
La arquitectura hexagonal es tan fuerte como la disciplina del equipo. Para preservar la estructura:
Estas prácticas refuerzan la dirección de dependencias, facilitan la lectura del repositorio y permiten que la arquitectura hexagonal cumpla su promesa de resiliencia ante cambios tecnológicos.