La arquitectura hexagonal, propuesta por Alistair Cockburn en 2005, también conocida como Puertos y Adaptadores, nació para resolver un problema crónico de la arquitectura en capas: que el dominio acababa dependiendo de la infraestructura (la base de datos, el framework web, los servicios externos). La idea central es brillante por su simplicidad: aísla la lógica de negocio en un núcleo que no sabe nada del mundo exterior, y conéctalo a ese mundo mediante puertos (interfaces) que se enchufan a adaptadores intercambiables. Así, puedes cambiar la base de datos, el framework o el protocolo sin tocar una sola línea de tu dominio. En esta lección entenderemos el núcleo, los puertos primarios y secundarios, los adaptadores, y construiremos un ejemplo completo en Java.
Contenido
- El problema que resuelve la hexagonal
- El núcleo de dominio
- Puertos primarios y secundarios
- Adaptadores
- La regla de dependencia y la inversión
- Ejemplo completo en Java: puerto + adaptador
- Ventajas e inconvenientes
- Errores comunes y consejos
- Ejercicios
- Conclusión
- El problema que resuelve la hexagonal
En la arquitectura en capas clásica, las dependencias apuntan hacia abajo: presentación → negocio → persistencia. Eso significa que el negocio depende de la persistencia, y la base de datos termina condicionando el modelo de dominio.
La arquitectura hexagonal invierte la situación: coloca el dominio en el centro y hace que todo lo demás dependa de él, nunca al revés.
graph TB
subgraph Exterior
UI[Adaptador Web/UI]
CLI[Adaptador CLI/Test]
DB[(Adaptador Base de Datos)]
EXT[Adaptador Servicio Externo]
end
subgraph Hexagono[Núcleo de Dominio]
PP[Puertos Primarios]
APP[Lógica de aplicación + Dominio]
PS[Puertos Secundarios]
PP --> APP --> PS
end
UI --> PP
CLI --> PP
PS --> DB
PS --> EXTLa figura del hexágono es solo simbólica (no significan "seis lados"): representa que el núcleo tiene múltiples puntos de conexión con el exterior, todos a través de puertos.
- El núcleo de dominio
El núcleo contiene la razón de ser de la aplicación: las entidades, las reglas de negocio y los casos de uso. Su característica definitoria:
- No importa nada del exterior. Cero
importde Spring, JPA, HTTP, JDBC. Solo Java puro (o tu lenguaje) y tus propias abstracciones. - Es independiente de frameworks. Podrías compilar el dominio sin que exista ni base de datos ni servidor web.
- Es 100% testeable en aislamiento. No necesita levantar nada.
// Dominio puro: ninguna dependencia de infraestructura
package com.fiatc.dominio;
public class Poliza {
private final String cliente;
private final String riesgo;
private final double prima;
public Poliza(String cliente, String riesgo, double prima) {
if (prima <= 0) throw new IllegalArgumentException("Prima inválida");
this.cliente = cliente;
this.riesgo = riesgo;
this.prima = prima;
}
public double prima() { return prima; }
public String cliente() { return cliente; }
public String riesgo() { return riesgo; }
}Observa que Poliza valida su propio invariante (prima positiva) en el constructor y no conoce nada externo. Es un objeto de dominio puro.
- Puertos primarios y secundarios
Un puerto es una interfaz que define una frontera del núcleo. Hay dos tipos, según en qué dirección fluye la conversación:
| Tipo de puerto | También llamado | Quién lo usa | Quién lo implementa | Ejemplo |
|---|---|---|---|---|
| Primario (driving) | De entrada / API | El exterior llama al núcleo | El núcleo | "Contratar póliza" |
| Secundario (driven) | De salida / SPI | El núcleo llama al exterior | Un adaptador externo | "Guardar póliza", "Notificar" |
- Puerto primario: describe qué puede hacer la aplicación. El mundo exterior (un controlador, un test) lo invoca para pedir un caso de uso. Lo implementa el núcleo.
- Puerto secundario: describe qué necesita la aplicación del exterior (persistencia, notificaciones). El núcleo lo declara como interfaz y lo implementa un adaptador externo.
// Puerto PRIMARIO (de entrada): qué ofrece la aplicación
package com.fiatc.dominio.puertos.entrada;
import com.fiatc.dominio.Poliza;
public interface ContratarPolizaUseCase {
Poliza contratar(String cliente, String riesgo);
}
// Puerto SECUNDARIO (de salida): qué necesita la aplicación del exterior
package com.fiatc.dominio.puertos.salida;
import com.fiatc.dominio.Poliza;
public interface RepositorioPolizas {
Poliza guardar(Poliza poliza);
}Lo crucial: ambas interfaces viven dentro del núcleo. El núcleo posee sus puertos. El exterior se adapta a ellas, no al revés.
- Adaptadores
Un adaptador es el código que conecta un puerto con una tecnología concreta. Hay dos familias, simétricas a los puertos:
- Adaptadores primarios (driving): traducen una petición externa (HTTP, CLI, evento de cola, test) en una llamada al puerto primario. Ejemplo: un
@RestController. - Adaptadores secundarios (driven): implementan un puerto secundario usando una tecnología concreta (JPA, JDBC, un cliente HTTP a un servicio externo). Ejemplo: un repositorio JPA.
graph LR
HTTP[Petición HTTP] --> AP[Adaptador primario\nPolizaController]
AP --> PP[Puerto primario\nContratarPolizaUseCase]
PP --> SVC[Servicio de aplicación]
SVC --> PS[Puerto secundario\nRepositorioPolizas]
PS --> AS[Adaptador secundario\nRepositorioPolizasJpa]
AS --> BD[(BD)]La gran ventaja: puedes tener varios adaptadores para el mismo puerto. El puerto secundario RepositorioPolizas puede tener un adaptador JPA en producción y un adaptador en memoria en los tests, sin que el núcleo se entere.
- La regla de dependencia y la inversión
La regla de oro de la hexagonal: las dependencias siempre apuntan hacia el núcleo.
graph LR
Adaptadores -->|dependen de| Puertos
Puertos -.viven en.-> Nucleo[Núcleo]
Adaptadores -. nunca al revés .-x NucleoEsto se consigue con el Principio de Inversión de Dependencias (la "D" de SOLID):
- El núcleo declara la interfaz
RepositorioPolizas(lo que necesita). - El adaptador JPA implementa esa interfaz (cómo se cumple).
- En tiempo de ejecución, se inyecta el adaptador concreto en el núcleo.
Resultado: el flujo de control va del núcleo al adaptador (el núcleo llama a guardar), pero la dependencia de código va del adaptador al núcleo (el adaptador implementa la interfaz del núcleo). Esa inversión es lo que protege el dominio.
- Ejemplo completo en Java: puerto + adaptador
Montamos el caso de uso completo de "contratar póliza".
// 1) SERVICIO DE APLICACIÓN: implementa el puerto primario y usa el secundario
package com.fiatc.dominio.aplicacion;
import com.fiatc.dominio.Poliza;
import com.fiatc.dominio.puertos.entrada.ContratarPolizaUseCase;
import com.fiatc.dominio.puertos.salida.RepositorioPolizas;
public class ContratarPolizaService implements ContratarPolizaUseCase {
private final RepositorioPolizas repositorio; // puerto secundario (interfaz)
public ContratarPolizaService(RepositorioPolizas repositorio) {
this.repositorio = repositorio; // se inyecta un adaptador concreto
}
@Override
public Poliza contratar(String cliente, String riesgo) {
double prima = calcularPrima(riesgo); // regla de negocio
Poliza poliza = new Poliza(cliente, riesgo, prima);
return repositorio.guardar(poliza); // llama al puerto de salida
}
private double calcularPrima(String riesgo) {
return "ALTO".equals(riesgo) ? 1200 : 600; // lógica de dominio pura
}
}Análisis:
ContratarPolizaServicevive en el núcleo y solo conoce interfaces (RepositorioPolizas), nunca tecnologías.- Recibe el repositorio por constructor (inyección de dependencias): no lo crea, se lo dan.
// 2) ADAPTADOR PRIMARIO: traduce HTTP -> puerto primario
package com.fiatc.infraestructura.web;
import com.fiatc.dominio.puertos.entrada.ContratarPolizaUseCase;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/polizas")
class PolizaController {
private final ContratarPolizaUseCase useCase; // depende del puerto, no del service
PolizaController(ContratarPolizaUseCase useCase) { this.useCase = useCase; }
@PostMapping
PolizaDto contratar(@RequestBody ContratarRequest req) {
var poliza = useCase.contratar(req.cliente(), req.riesgo());
return new PolizaDto(poliza.cliente(), poliza.prima()); // traduce a DTO
}
}// 3) ADAPTADOR SECUNDARIO: implementa el puerto de salida con JPA
package com.fiatc.infraestructura.persistencia;
import com.fiatc.dominio.Poliza;
import com.fiatc.dominio.puertos.salida.RepositorioPolizas;
import org.springframework.stereotype.Repository;
@Repository
class RepositorioPolizasJpa implements RepositorioPolizas {
private final JpaPolizaDao dao; // tecnología concreta encapsulada aquí
RepositorioPolizasJpa(JpaPolizaDao dao) { this.dao = dao; }
@Override
public Poliza guardar(Poliza poliza) {
var entidad = PolizaEntity.desde(poliza); // mapeo dominio -> entidad JPA
dao.save(entidad);
return poliza;
}
}Y el cableado (composición):
// 4) CONFIGURACIÓN: aquí se "enchufan" los adaptadores a los puertos
@Configuration
class Configuracion {
@Bean
ContratarPolizaUseCase contratarPolizaUseCase(RepositorioPolizas repo) {
return new ContratarPolizaService(repo); // inyecta el adaptador JPA
}
}La clave del ejemplo: el paquete com.fiatc.dominio no importa nada de Spring ni de JPA. Toda la tecnología vive en com.fiatc.infraestructura. Si mañana cambias JPA por MongoDB, solo escribes un nuevo adaptador secundario; el núcleo no se toca.
Y para probar el caso de uso, ni siquiera necesitas base de datos:
// TEST: adaptador secundario en memoria, sin Spring ni BD
class ContratarPolizaServiceTest {
@Test
void contrata_y_guarda() {
var enMemoria = new RepositorioPolizas() {
Poliza ultima;
public Poliza guardar(Poliza p) { this.ultima = p; return p; }
};
var service = new ContratarPolizaService(enMemoria);
var poliza = service.contratar("ACME", "ALTO");
assertEquals(1200, poliza.prima());
}
}
- Ventajas e inconvenientes
| Ventajas | Inconvenientes |
|---|---|
| El dominio queda aislado y libre de frameworks | Más interfaces y clases (mayor "ceremonia") |
| Adaptadores intercambiables (JPA, Mongo, tests) | Curva de aprendizaje para el equipo |
| Tests del núcleo sin infraestructura | Puede ser sobreingeniería en CRUDs triviales |
| Inversión de dependencias clara | Necesita disciplina para no "colar" tecnología en el núcleo |
| Facilita migrar tecnologías sin tocar negocio | Mapeos dominio↔entidad adicionales |
- Errores Comunes y Consejos
- Colar dependencias de framework en el núcleo. Si ves un
import org.springframeworkojavax.persistencedentro del dominio, has roto la hexagonal. - Confundir puerto primario y secundario. Primario = el exterior te llama (lo implementa el núcleo). Secundario = tú llamas al exterior (lo implementa un adaptador).
- Usar la entidad JPA como objeto de dominio. Acopla el dominio a la persistencia. Mantén entidades de dominio y de persistencia separadas, con mapeo entre ambas.
- Aplicarla a todo. En un CRUD simple sin reglas, la hexagonal puede ser sobreingeniería. Reserva el esfuerzo para dominios ricos.
- Consejo: usa pruebas de arquitectura (p. ej. ArchUnit) para verificar automáticamente que el paquete
dominiono importa nada de infraestructura.
- Ejercicios
Ejercicio 1. Clasifica cada puerto como primario o secundario: (a) EnviarNotificacion; (b) ConsultarSaldoUseCase; (c) RepositorioClientes; (d) PasarelaPago.
Ejercicio 2. Tu núcleo necesita enviar un correo al contratar una póliza. Diseña el puerto y nombra dos adaptadores posibles (uno de producción y uno de test).
Ejercicio 3. Explica por qué el ContratarPolizaService puede probarse sin levantar la base de datos.
Soluciones
Solución 1. (a) Secundario (el núcleo llama al exterior para notificar). (b) Primario (el exterior invoca un caso de uso). (c) Secundario (persistencia). (d) Secundario (servicio externo de pago).
Solución 2. Puerto secundario:
public interface NotificadorEmail {
void enviar(String destinatario, String asunto, String cuerpo);
}Adaptadores: (1) producción → NotificadorSmtp que envía por SMTP; (2) test → NotificadorEnMemoria que guarda los correos en una lista para verificarlos.
Solución 3. Porque ContratarPolizaService depende de la interfaz RepositorioPolizas, no de su implementación JPA. En el test se le inyecta un adaptador en memoria, de modo que se ejercita toda la lógica de negocio sin tocar infraestructura.
- Conclusión
La arquitectura hexagonal sitúa el dominio en el centro y lo protege del exterior mediante puertos (interfaces que posee el núcleo) y adaptadores (implementaciones intercambiables que dependen del núcleo). Gracias a la inversión de dependencias, el negocio queda libre de frameworks, es plenamente testeable y permite cambiar tecnologías sin tocar la lógica. Hemos construido un ejemplo completo de puerto primario, puerto secundario y sus adaptadores. Estas mismas ideas —dominio en el centro, dependencias hacia adentro— son el fundamento de los estilos que veremos a continuación: la Arquitectura Limpia y la Arquitectura Cebolla (Clean & Onion), que formalizan la regla de dependencia en círculos concéntricos y que compararemos con la hexagonal.
Curso de Arquitectura de Aplicaciones
Módulo 1: Fundamentos de la Arquitectura de Aplicaciones
- ¿Qué es la Arquitectura de Aplicaciones?
- El Rol del Arquitecto de Software
- Atributos de Calidad y Requisitos No Funcionales
- Decisiones Arquitectónicas y Compromisos (Trade-offs)
- Documentación de Arquitectura: Vistas y el Modelo C4
Módulo 2: Principios y Tácticas de Diseño
- Acoplamiento, Cohesión y Separación de Responsabilidades
- Principios SOLID Aplicados a la Arquitectura
- DRY, KISS, YAGNI y Otros Principios de Diseño
- Tácticas Arquitectónicas para los Atributos de Calidad
- Gestión de la Deuda Técnica
Módulo 3: Estilos y Patrones Arquitectónicos
- Arquitectura Monolítica
- Arquitectura en Capas (N-Tier)
- Arquitectura Cliente-Servidor
- Arquitectura Hexagonal (Puertos y Adaptadores)
- Arquitectura Limpia y Cebolla (Clean & Onion)
Módulo 4: Arquitecturas Distribuidas y Microservicios
- Introducción a los Sistemas Distribuidos
- Arquitectura de Microservicios
- Descomposición de Servicios y Bounded Contexts
- API Gateway, Service Discovery y Comunicación entre Servicios
- Patrones de Resiliencia: Circuit Breaker, Retry y Bulkhead
- El Teorema CAP y la Consistencia de Datos
Módulo 5: Arquitecturas Dirigidas por Eventos y Mensajería
- Fundamentos de la Arquitectura Orientada a Eventos
- Mensajería Asíncrona: Colas y Brokers
- Patrones de Eventos: Event Sourcing y CQRS
- Gestión de Transacciones Distribuidas: Patrón Saga
- Streaming de Datos en Tiempo Real
Módulo 6: Diseño Dirigido por el Dominio (DDD)
- Conceptos Fundamentales del DDD
- Diseño Estratégico: Bounded Contexts y Lenguaje Ubicuo
- Diseño Táctico: Entidades, Agregados y Repositorios
- Mapeo de Contextos (Context Mapping)
Módulo 7: Datos y Persistencia
- Estrategias de Persistencia: SQL vs NoSQL
- Patrones de Acceso a Datos: Repository, Unit of Work y DAO
- Base de Datos por Servicio y Gestión de Datos Distribuidos
- Caché y Estrategias de Invalidación
Módulo 8: Arquitectura en la Nube y Despliegue
- Fundamentos del Cloud Computing (IaaS, PaaS, SaaS)
- Contenedores y Orquestación con Docker y Kubernetes
- Arquitectura Serverless
- Patrones de Diseño Cloud-Native
- Infraestructura como Código (IaC)
Módulo 9: Calidad, Seguridad y Observabilidad
- Escalabilidad: Horizontal vs Vertical y Balanceo de Carga
- Alta Disponibilidad y Tolerancia a Fallos
- Seguridad por Diseño y Autenticación/Autorización
- Observabilidad: Logging, Métricas y Trazabilidad
- Rendimiento y Pruebas de Carga
