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

  1. El problema que resuelve la hexagonal
  2. El núcleo de dominio
  3. Puertos primarios y secundarios
  4. Adaptadores
  5. La regla de dependencia y la inversión
  6. Ejemplo completo en Java: puerto + adaptador
  7. Ventajas e inconvenientes
  8. Errores comunes y consejos
  9. Ejercicios
  10. Conclusión

  1. 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 --> EXT

La 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.

  1. 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 import de 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.

  1. 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.

  1. 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.

  1. 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 Nucleo

Esto 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.

  1. 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:

  • ContratarPolizaService vive 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());
    }
}

  1. 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

  1. Errores Comunes y Consejos

  • Colar dependencias de framework en el núcleo. Si ves un import org.springframework o javax.persistence dentro 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 dominio no importa nada de infraestructura.

  1. 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.

  1. 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

Módulo 2: Principios y Tácticas de Diseño

Módulo 3: Estilos y Patrones Arquitectónicos

Módulo 4: Arquitecturas Distribuidas y Microservicios

Módulo 5: Arquitecturas Dirigidas por Eventos y Mensajería

Módulo 6: Diseño Dirigido por el Dominio (DDD)

Módulo 7: Datos y Persistencia

Módulo 8: Arquitectura en la Nube y Despliegue

Módulo 9: Calidad, Seguridad y Observabilidad

Módulo 10: Evolución, Gobernanza y Casos Prácticos

© Copyright 2026. Todos los derechos reservados