Día 27 de 30 Fase 7: API Gateway, API-First, Arquitectura Hexagonal Enterprise y Sagas

Arquitectura Hexagonal (Ports & Adapters) y Clean Architecture

Dominio Aislado, Puertos de Entrada/Salida, Adaptadores de Infraestructura y Paquetes de DDD

Para dominar. Al terminar deberías poder hacerlo sin ayuda.

  • 4h de estudio
  • 5 conceptos
  • 3 preguntas
  • 1 katas
HexagonalPorts & AdaptersClean ArchitectureDDDInversión de Dependencias
Al terminar este día sabrás 4
  • Aplicar la regla de dependencia: el dominio no conoce la infraestructura.
  • Definir puertos de entrada como casos de uso y puertos de salida como necesidades del dominio.
  • Implementar adaptadores primarios y secundarios, con mapeo entre modelos.
  • Organizar paquetes y proteger la arquitectura con tests automáticos.

El Principio Fundamental: El Dominio No Conoce la Infraestructura

En Arquitectura Hexagonal (creada por Alistair Cockburn), el código de negocio (dominio) vive en el centro del sistema y NO depende de frameworks, bases de datos, HTTP ni mensajería. La regla de dependencia es estricta: las capas externas (infraestructura) dependen del centro (dominio), NUNCA al revés.

En una línea public interface PedidoRepository { void guardar(Pedido pedido); Optional<Pedido> buscarPorId(UUID id); } // Interfaz PURA del dominio, sin @Repository ni Spring

Dominio en Java puro java
package com.acme.taskflow.tareas.dominio;

// Sin imports de Spring, JPA ni Jackson
public final class Tarea {
    private final TareaId id;
    private final ProyectoId proyecto;
    private String titulo;
    private EstadoTarea estado;
    private Responsable responsable;

    public static Tarea nueva(TareaId id, ProyectoId proyecto, String titulo) {
        return new Tarea(id, proyecto, titulo, EstadoTarea.PENDIENTE, null);
    }

    public void asignarA(Responsable nuevo) {
        if (estado == EstadoTarea.COMPLETADA) {
            throw new TareaCerradaException(id);
        }
        this.responsable = Objects.requireNonNull(nuevo);
    }

    public void completar() {
        if (responsable == null) throw new TareaSinResponsableException(id);
        this.estado = EstadoTarea.COMPLETADA;
    }

    // constructor completo, getters de solo lectura...
}

public record TareaId(UUID valor) {
    public static TareaId nuevo() { return new TareaId(UUID.randomUUID()); }
}
Cómo funciona por dentro

La idea central de la arquitectura hexagonal (Alistair Cockburn) es que la aplicación tiene un interior, el dominio y los casos de uso, y un exterior, todo lo que es tecnología: HTTP, bases de datos, colas, proveedores externos, la línea de comandos. El interior no depende del exterior; es el exterior el que se adapta al interior.

En código, eso significa que las clases del dominio no importan nada de org.springframework, jakarta.persistence, com.fasterxml.jackson ni clientes HTTP. Son Java puro: entidades con comportamiento, objetos de valor, excepciones y servicios de dominio.

El beneficio es doble. El dominio se prueba con tests unitarios rápidos, sin contexto de Spring ni contenedores. Y las decisiones tecnológicas se pueden cambiar o posponer, porque están confinadas en los bordes. El coste es más código de mapeo y más estructura, que solo compensa cuando hay lógica de negocio real que proteger.

Permite cambiar la base de datos (de PostgreSQL a MongoDB), el framework web (de Spring a Quarkus) o el sistema de mensajería (de Kafka a RabbitMQ) sin tocar una sola línea del código de negocio.

Se conecta con SOLIDInversión de Dependencias (DIP)Testabilidad

Cuatro bloques cronometrados. No pases al siguiente sin tener el resultado esperado.

  1. Bloque 1: Principios de Arquitectura Hexagonal (0 - 25 min)

    Qué estudiar. Regla de dependencias, puertos de entrada y salida, diferencia con MVC clásico.

    Pregunta a responder. ¿En qué dirección van las dependencias en Hexagonal?

    Resultado esperado. Diagrama hexagonal con dominio en el centro y adaptadores alrededor.

  2. Bloque 2: Implementación Práctica en Java/Spring (25 - 50 min)

    Qué estudiar. Creación de un puerto de salida (interfaz), un servicio de aplicación (use case) y un adaptador JPA.

    Pregunta a responder. ¿Cómo inyecta Spring el adaptador concreto en el puerto del dominio?

    Resultado esperado. Módulo de pedidos con dominio puro y adaptador JPA funcional.

  3. Bloque 3: Testing y Validación de Arquitectura con ArchUnit (50 - 80 min)

    Qué estudiar. Tests unitarios del dominio sin Spring, tests de arquitectura con ArchUnit.

    Pregunta a responder. ¿Cómo asegurar que nadie acouple el dominio a Spring en el futuro?

    Resultado esperado. Test ArchUnit que valide la regla de dependencias hexagonal.

  4. Cierre y Consolidación (80 - 90 min)

    Qué estudiar. Cuándo usar y cuándo NO usar Hexagonal.

    Pregunta a responder. ¿Qué criterios usarías para recomendar Hexagonal a un nuevo proyecto?

    Resultado esperado. Checklist de decisión en notas personales.

Tres formas de comprobar si lo has entendido, de menos a más exigente: explicarlo con tus palabras, responder preguntas trampa y resolver el reto del tutor.

Explícalo desde cero

Lee cada ficha y explícala en voz alta antes de seguir: si puedes contárselo a alguien que empieza, lo has entendido.

Por qué el dominio NUNCA debe importar clases de Spring, JPA o Kafka

Lo que debes entender

Si tu clase Pedido tiene @Entity y @Column de JPA, tu dominio está casado con Hibernate. Si mañana quieres migrar a MongoDB o testear sin base de datos, tendrás que modificar el dominio. En Hexagonal, el dominio es un JAR de Java puro sin dependencias externas. Los adaptadores (en otro paquete) traducen entre el modelo de dominio y la tecnología concreta.

Analogía para contarlo

El dominio es como un enchufe estándar de pared (interfaz). No le importa si conectas una lámpara LED, una tostadora o un cargador (adaptadores). El enchufe define el contrato eléctrico; cada aparato se adapta a ese contrato.

Lo que suele explicarse mal

Decir que Hexagonal "es poner carpetas" o "es lo mismo que MVC con más capas". La diferencia es la dirección de las dependencias: en MVC el controlador llama al servicio que llama al repository de Spring Data. En Hexagonal, el dominio define interfaces (puertos) y la infraestructura las implementa.

Señal de que aún no lo dominas

Si tu clase de dominio Pedido.java importa javax.persistence.Entity o org.springframework.data.annotation.Id.

Preguntas trampa

Responde de memoria, en voz alta o por escrito, y después abre la respuesta razonada: lo importante es el porqué, no la palabra exacta.

Nivel 1 (Básico) ¿Cuál es la diferencia entre un Puerto y un Adaptador en Arquitectura Hexagonal? Ver respuesta

Respuesta razonada

Un puerto es una interfaz que pertenece al interior de la aplicación y define un punto de interacción en términos del negocio: qué ofrece la aplicación (puerto de entrada, un caso de uso) o qué necesita del exterior (puerto de salida, por ejemplo cargar una tarea). No sabe nada de tecnología.

Un adaptador es la pieza concreta que conecta ese puerto con una tecnología. Un adaptador primario (un controlador REST, un listener de Kafka) traduce estímulos externos y llama a un puerto de entrada. Un adaptador secundario (un repositorio JPA, un cliente HTTP) implementa un puerto de salida. Para una misma interfaz de puerto puede haber varios adaptadores intercambiables: uno con JPA en producción y otro en memoria en tests.

Qué está evaluando la pregunta

Un puerto es una interfaz Java pura que vive en el dominio y define un contrato (qué operaciones se necesitan). Un adaptador es la implementación concreta de ese contrato que vive en la capa de infraestructura y usa una tecnología específica (JPA, Kafka, HTTP).

Nivel 2 (Intermedio) ¿Por qué en Hexagonal se necesitan dos modelos distintos: uno de dominio y otro de persistencia (JPA Entity)? Ver respuesta

Respuesta razonada

Porque tienen responsabilidades distintas y cada tecnología impone restricciones. La entidad JPA necesita un constructor vacío, campos mutables, anotaciones de mapeo, relaciones con carga perezosa y a veces una forma dictada por el esquema de tablas. El modelo de dominio quiere invariantes protegidos, objetos de valor (TareaId, Email), inmutabilidad donde tenga sentido y ninguna dependencia de frameworks.

Si usas una sola clase para ambas cosas, las necesidades de persistencia acaban deformando el dominio (setters públicos, ids nulos, colecciones perezosas que fallan fuera de la transacción). Con dos modelos, cada uno evoluciona por su lado y un mapper los conecta. El coste es ese código de mapeo. En dominios sencillos es razonable usar la entidad JPA como modelo de dominio y asumir el compromiso conscientemente.

Qué está evaluando la pregunta

El modelo de dominio representa las reglas de negocio puras sin restricciones técnicas. El modelo de persistencia (JPA Entity) tiene anotaciones de mapeo ORM, campos técnicos como versiones optimistas, y puede estar normalizado de forma diferente. El adaptador de persistencia traduce entre ambos modelos, evitando que las restricciones de la base de datos contaminen la lógica de negocio.

Nivel 3 (Entrevista Senior) ¿Cómo validarías con tests automatizados que ninguna clase del paquete domain importe clases de Spring o JPA? Ver respuesta

Respuesta razonada

Con ArchUnit, una librería que analiza el bytecode de tus clases y permite expresar reglas de arquitectura como tests de JUnit. Por ejemplo: noClasses().that().resideInAPackage("..dominio..").should().dependOnClassesThat().resideInAnyPackage("org.springframework..", "jakarta.persistence..", "com.fasterxml.jackson.."), ejecutada sobre new ClassFileImporter().importPackages("com.acme.taskflow").

El test falla en cuanto alguien añade un import prohibido, en la integración continua y antes de llegar a revisión. ArchUnit también permite comprobar capas completas con layeredArchitecture() o la librería Architectures.onionArchitecture(). Una alternativa más estricta es separar el dominio en un módulo Maven sin dependencias de Spring: entonces el import ni siquiera compila.

Qué está evaluando la pregunta

Usando la librería ArchUnit: ArchRuleDefinition.noClasses().that().resideInAPackage("..domain..").should().dependOnClassesThat().resideInAnyPackage("org.springframework..", "javax.persistence..", "jakarta.persistence.."). Este test falla automáticamente en CI si alguien introduce un import prohibido.

Reto del tutor

Un desarrollador senior defiende: "Arquitectura Hexagonal es over-engineering innecesario. Con Spring Boot, @Service y @Repository ya tengo separación de capas suficiente". Argumenta técnicamente cuándo tiene razón y cuándo no.

Lo que una respuesta suele tener bien

Reconoce que tiene parte de razón para proyectos simples.

Lo que suele quedarse en la superficie

Decir "siempre se debe usar Hexagonal".

El concepto de fondo que suele faltar

No distinguir entre complejidad accidental y esencial. En un microservicio CRUD de 5 endpoints que nunca cambiará de tecnología, Hexagonal añade indirección innecesaria.

Cómo lo respondería alguien senior en una entrevista

Tiene razón para microservicios CRUD simples de corta vida o prototipos donde la probabilidad de cambiar la infraestructura es nula y la complejidad de dominio es baja: @Service + @Repository de Spring Boot es perfectamente suficiente. NO tiene razón cuando: (1) El dominio tiene reglas de negocio complejas que deben testearse unitariamente sin Spring ni BD; (2) Múltiples adaptadores de entrada (REST + Kafka + gRPC) consumen el mismo dominio; (3) Es probable migrar la persistencia (PostgreSQL a DocumentDB); (4) El equipo es grande y necesita límites claros entre infraestructura y dominio para evitar acoplamiento progresivo. La decisión correcta es evaluar la complejidad del dominio y la probabilidad de cambio tecnológico.

Proyecto «Pedidos» · paso 27 de 30

Arquitectura hexagonal

Que el dominio de pedidos no dependa de Spring, JPA ni Kafka.

De qué va el proyecto

El backend de una tienda online: clientes, catálogo de productos y pedidos con sus líneas, pagos y envíos. Empieza como un proyecto Maven con records y termina como un sistema con API REST, seguridad, mensajería, contenedores y observabilidad. Cada paso construye sobre el anterior, así que trabaja siempre en el mismo repositorio.

Qué hacer

  1. Reorganiza el código en dominio, aplicacion e infraestructura.
  2. Define puertos de salida (PedidoRepository, PublicadorEventos) en el dominio y sus adaptadores en infraestructura.
  3. Añade un test de ArchUnit que impida que el paquete dominio importe Spring o Jakarta.

Está listo cuando

  • Los tests del dominio se ejecutan sin ninguna dependencia de Spring.
  • El test de ArchUnit falla si alguien añade una anotación de JPA al dominio.

Katas: ejercicios cortos

Reglas de oro

  • El paquete domain jamás debe importar clases de Spring, JPA, Kafka ni ningún framework.
  • Define interfaces (puertos) en el dominio; implementa adaptadores en infraestructura.
  • Usa dos modelos separados: modelo de dominio puro y modelo de persistencia (JPA Entity).
  • Valida la regla de dependencias en CI con tests ArchUnit automatizados.
  • No apliques Hexagonal a CRUDs simples sin complejidad de dominio; usa MVC clásico y no sobre-ingenies.
Se guarda automáticamente

Fuentes oficiales

Revisado el . Este día en Markdown