# Día 27: Arquitectura Hexagonal (Ports & Adapters) y Clean Architecture

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

- Plan: [JavaMastery](https://aprende-java.webs-economicas.es/), día 27 de 30
- Fase 7: API Gateway, API-First, Arquitectura Hexagonal Enterprise y Sagas
- Dedicación: 4h
- Profundidad: para dominar
- Revisado: 2026-10-29
- Versión interactiva: https://aprende-java.webs-economicas.es/dias/arquitectura-hexagonal-clean/

## Al terminar este día sabrás

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


## Conceptos

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

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

*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()); }
}
```

- **En una línea:** `public interface PedidoRepository { void guardar(Pedido pedido); Optional<Pedido> buscarPorId(UUID id); } // Interfaz PURA del dominio, sin @Repository ni Spring`
- **Por qué importa:** 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.
- **Error típico:** Poner anotaciones de Spring (`@Service`, `@Autowired`) o de JPA (`@Entity`, `@Column`) en las clases del dominio, acoplándolas a la infraestructura.
- **Java 17 frente a 21:** Patrón independiente de versión.
- **Se conecta con:** SOLID, Inversión de Dependencias (DIP), Testabilidad

### 2. Puertos de Entrada (Driving Ports): Los Casos de Uso

Un puerto de entrada es una interfaz que define las operaciones que el mundo exterior puede invocar sobre el dominio. Representa los **casos de uso** de la aplicación: `CrearPedidoUseCase`, `ConsultarInventarioUseCase`. Las implementaciones de estos puertos son los **servicios de aplicación** (o use cases) que orquestan la lógica del dominio.

**Cómo funciona por dentro.**

Un puerto de entrada (driving port) define lo que la aplicación ofrece al exterior: sus casos de uso. Se expresa como una interfaz con un método por operación de negocio, que recibe un comando (un record con los datos de entrada, ya validados en su forma) y devuelve un resultado del dominio o nada.

El servicio de aplicación implementa ese puerto: carga los agregados a través de los puertos de salida, invoca la lógica del dominio, guarda los cambios y publica eventos. No contiene reglas de negocio (están en el dominio); orquesta. Aquí suele vivir el límite transaccional.

Tener el caso de uso como interfaz hace explícito el catálogo de operaciones del sistema: leyendo el paquete de puertos de entrada sabes qué puede hacer la aplicación, sin buscar entre controladores.

*Puerto de entrada y su implementación*

```java
package com.acme.taskflow.tareas.aplicacion.puertos.entrada;

public interface AsignarTareaUseCase {
    void asignar(AsignarTareaCommand comando);

    record AsignarTareaCommand(TareaId tareaId, String emailResponsable) {
        public AsignarTareaCommand {
            Objects.requireNonNull(tareaId);
            if (emailResponsable == null || !emailResponsable.contains("@")) {
                throw new IllegalArgumentException("Email no válido");
            }
        }
    }
}

package com.acme.taskflow.tareas.aplicacion;

public class AsignarTareaService implements AsignarTareaUseCase {
    private final CargarTareaPort cargar;
    private final GuardarTareaPort guardar;
    private final BuscarMiembroPort miembros;

    public AsignarTareaService(CargarTareaPort cargar, GuardarTareaPort guardar, BuscarMiembroPort miembros) {
        this.cargar = cargar;
        this.guardar = guardar;
        this.miembros = miembros;
    }

    @Override
    public void asignar(AsignarTareaCommand cmd) {
        Tarea tarea = cargar.cargar(cmd.tareaId())
            .orElseThrow(() -> new TareaNoEncontradaException(cmd.tareaId()));
        Responsable r = miembros.porEmail(cmd.emailResponsable())
            .orElseThrow(() -> new MiembroNoEncontradoException(cmd.emailResponsable()));
        tarea.asignarA(r);                     // la regla está en el dominio
        guardar.guardar(tarea);
    }
}
```

- **En una línea:** `public interface CrearPedidoUseCase { PedidoId ejecutar(CrearPedidoCommand comando); }`
- **Por qué importa:** Expone las capacidades del dominio de forma explícita sin revelar los detalles internos de implementación.
- **Error típico:** No definir interfaces explícitas para los casos de uso y acoplar los controladores REST directamente a los servicios de dominio.
- **Java 17 frente a 21:** Aplica a cualquier versión.
- **Se conecta con:** Use Cases, Capa de Aplicación, Controladores REST

### 3. Puertos de Salida (Driven Ports): Las Dependencias del Dominio

Un puerto de salida es una interfaz que el dominio define pero que será implementada por la infraestructura: repositorios de persistencia, clientes HTTP externos, publicadores de eventos, servicios de notificación. El dominio define QUÉ necesita; la infraestructura decide CÓMO lo provee.

**Cómo funciona por dentro.**

Un puerto de salida (driven port) describe algo que la aplicación necesita del exterior, expresado en el lenguaje del dominio: «cargar una tarea», «guardar una tarea», «buscar un miembro por email», «notificar una asignación». No dice nada de SQL, HTTP ni Kafka.

Los puertos de salida pertenecen al interior: se definen junto a los casos de uso (o en el dominio) y los implementan los adaptadores. Esa es la inversión de dependencias aplicada a la arquitectura: la flecha de dependencia del código va del adaptador al puerto, en sentido contrario al flujo de la llamada.

Conviene que sean pequeños y específicos (`CargarTareaPort`, `GuardarTareaPort`) en lugar de un repositorio genérico con veinte métodos. Así cada caso de uso declara exactamente lo que usa y los dobles de test son triviales.

*Puertos de salida en lenguaje del dominio*

```java
package com.acme.taskflow.tareas.aplicacion.puertos.salida;

public interface CargarTareaPort {
    Optional<Tarea> cargar(TareaId id);
}

public interface GuardarTareaPort {
    void guardar(Tarea tarea);
}

public interface BuscarMiembroPort {
    Optional<Responsable> porEmail(String email);
}

public interface NotificarAsignacionPort {
    void tareaAsignada(Tarea tarea, Responsable responsable);
}

// En los tests de aplicación: dobles de una línea
class TareasEnMemoria implements CargarTareaPort, GuardarTareaPort {
    final Map<TareaId, Tarea> datos = new HashMap<>();
    public Optional<Tarea> cargar(TareaId id) { return Optional.ofNullable(datos.get(id)); }
    public void guardar(Tarea t) { datos.put(t.id(), t); }
}
```

- **En una línea:** `// Puerto (dominio): interface PedidoRepository { void guardar(Pedido p); }
// Adaptador (infra): class JpaPedidoAdapter implements PedidoRepository { private JpaPedidoJpaRepo repo; ... }`
- **Por qué importa:** Aplicación pura del Principio de Inversión de Dependencias (DIP de SOLID): la capa de alto nivel (dominio) no depende de la capa de bajo nivel (infraestructura). Ambas dependen de abstracciones (interfaces).
- **Error típico:** Usar directamente `JpaRepository` de Spring Data en el dominio en vez de definir un puerto propio.
- **Java 17 frente a 21:** Aplica a cualquier versión.
- **Se conecta con:** Repository Pattern, Gateway Pattern, Principio DIP

### 4. Adaptadores: La Implementación Concreta de los Puertos

Un adaptador es la implementación concreta de un puerto que conecta el dominio con una tecnología específica. Adaptadores de entrada: `PedidoRestController` (HTTP), `PedidoKafkaListener` (mensajería), `PedidoGrpcService` (gRPC). Adaptadores de salida: `JpaPedidoAdapter` (BD relacional), `KafkaPedidoEventPublisher` (broker), `SmtpNotificacionAdapter` (email).

**Cómo funciona por dentro.**

Los adaptadores conectan los puertos con la tecnología. Los primarios (driving) reciben estímulos del exterior y llaman a puertos de entrada: un `@RestController`, un `@KafkaListener`, una tarea `@Scheduled` o un comando de consola. Traducen el formato externo (JSON, mensaje) a un comando y el resultado a una respuesta.

Los secundarios (driven) implementan puertos de salida: un adaptador de persistencia con Spring Data JPA, un cliente HTTP hacia otro servicio o un productor de Kafka. Traducen del modelo del dominio al de la tecnología y viceversa. Aquí viven las entidades JPA, que son un detalle de persistencia, no el modelo de dominio.

Toda la complejidad tecnológica queda en los adaptadores, que se prueban con tests de integración (Testcontainers, `@WebMvcTest`). Si mañana cambias JPA por `JdbcClient`, solo escribes un adaptador nuevo.

*Adaptador primario REST y secundario JPA*

```java
// Adaptador primario: HTTP -> comando -> caso de uso
@RestController
@RequestMapping("/api/tareas")
class TareaWebAdapter {
    private final AsignarTareaUseCase asignar;
    TareaWebAdapter(AsignarTareaUseCase asignar) { this.asignar = asignar; }

    @PutMapping("/{id}/responsable")
    @ResponseStatus(HttpStatus.NO_CONTENT)
    void asignar(@PathVariable UUID id, @RequestBody @Valid AsignarRequest body) {
        asignar.asignar(new AsignarTareaCommand(new TareaId(id), body.email()));
    }
}

// Adaptador secundario: puerto de salida -> JPA
@Component
class TareaPersistenciaAdapter implements CargarTareaPort, GuardarTareaPort {
    private final TareaJpaRepository jpa;
    TareaPersistenciaAdapter(TareaJpaRepository jpa) { this.jpa = jpa; }

    @Override
    public Optional<Tarea> cargar(TareaId id) {
        return jpa.findById(id.valor()).map(TareaJpaMapper::aDominio);
    }

    @Override
    public void guardar(Tarea tarea) {
        jpa.save(TareaJpaMapper.aEntidad(tarea));    // TareaJpaEntity: @Entity, solo aquí
    }
}
```

- **En una línea:** `com.app.domain.port.PedidoRepository (interfaz) -> com.app.infrastructure.adapter.persistence.JpaPedidoAdapter (implementación con JPA)`
- **Por qué importa:** Los adaptadores son piezas intercambiables. Puedes tener un adaptador JPA para producción y un adaptador InMemory para tests unitarios, sin tocar el dominio.
- **Error típico:** No separar los adaptadores en paquetes distintos, mezclando código de infraestructura con lógica de dominio en el mismo directorio.
- **Java 17 frente a 21:** Aplica a cualquier versión.
- **Se conecta con:** Spring Beans, Testcontainers, Mocking

### 5. Estructura de Paquetes para Arquitectura Hexagonal en Java

Paquetes recomendados: `domain.model` (entidades, value objects, records de dominio puro), `domain.port.in` (puertos de entrada / use cases), `domain.port.out` (puertos de salida / repositorios, gateways), `application.service` (implementaciones de los use cases), `infrastructure.adapter.in.rest` (controladores HTTP), `infrastructure.adapter.out.persistence` (JPA adapters), `infrastructure.adapter.out.messaging` (Kafka/RabbitMQ adapters), `infrastructure.config` (Spring configuration).

**Cómo funciona por dentro.**

Una estructura que funciona bien es agrupar primero por funcionalidad (módulo de negocio) y dentro de cada una por capa hexagonal. Así todo lo relacionado con tareas está junto, y dentro se ve claramente qué es dominio, qué es aplicación y qué son adaptadores.

Para que el dominio y los casos de uso no dependan de Spring, ni siquiera por una anotación `@Service`, se pueden registrar como beans desde una clase `@Configuration` en la capa de infraestructura. Es opcional: muchos equipos aceptan `@Service` en la capa de aplicación como compromiso pragmático, siempre que el dominio quede limpio.

Los paquetes por sí solos no impiden que alguien importe una clase de JPA en el dominio. Para eso se usan tests de arquitectura (ArchUnit) o módulos de Maven separados, que convierten la regla en un error de compilación.

*Estructura de paquetes y cableado sin anotaciones en el núcleo*

```java
/*
com.acme.taskflow
└── tareas
    ├── dominio                     Tarea, TareaId, Responsable, excepciones
    ├── aplicacion
    │   ├── puertos
    │   │   ├── entrada             AsignarTareaUseCase, CompletarTareaUseCase
    │   │   └── salida              CargarTareaPort, GuardarTareaPort, NotificarAsignacionPort
    │   └── AsignarTareaService     (sin anotaciones de Spring)
    └── adaptadores
        ├── entrada
        │   ├── web                 TareaWebAdapter, DTOs de la API
        │   └── mensajeria          TareaEventosListener
        ├── salida
        │   ├── persistencia        TareaPersistenciaAdapter, TareaJpaEntity, TareaJpaRepository
        │   └── notificaciones      NotificadorEmailAdapter
        └── config                  TareasConfig (cableado)
*/

@Configuration
class TareasConfig {
    @Bean
    AsignarTareaUseCase asignarTarea(CargarTareaPort cargar, GuardarTareaPort guardar,
                                     BuscarMiembroPort miembros) {
        return new AsignarTareaService(cargar, guardar, miembros);   // el núcleo no conoce Spring
    }
}
```

- **En una línea:** `src/main/java/com/app/pedidos/domain/model/Pedido.java
src/main/java/com/app/pedidos/domain/port/out/PedidoRepository.java
src/main/java/com/app/pedidos/infrastructure/adapter/out/persistence/JpaPedidoAdapter.java`
- **Por qué importa:** Hace visible en la estructura de directorios la separación de capas, facilitando code reviews y onboarding de nuevos desarrolladores.
- **Error típico:** Organizar por tipo técnico (`controllers/`, `services/`, `repositories/`) en vez de por módulo de dominio y capa hexagonal.
- **Java 17 frente a 21:** Aplica a cualquier versión.
- **Se conecta con:** Convenciones de Paquetes, Módulos Java 9+, ArchUnit


## Sesión de 90 minutos

### 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?
- **Entregable:** Diagrama hexagonal con dominio en el centro y adaptadores alrededor.

### 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?
- **Entregable:** Módulo de pedidos con dominio puro y adaptador JPA funcional.

### 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?
- **Entregable:** Test ArchUnit que valide la regla de dependencias hexagonal.

### 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?
- **Entregable:** Checklist de decisión en notas personales.


## Explícalo desde cero

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

- **Qué 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.
- **Qué 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.
- **Ejemplo sencillo:** 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.
- **Señal de alerta:** Si tu clase de dominio `Pedido.java` importa `javax.persistence.Entity` o `org.springframework.data.annotation.Id`.


## Preguntas

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

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

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

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

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.

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

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

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.

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


## Reto del tutor

**Reto:** 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 está bien:** Reconoce que tiene parte de razón para proyectos simples.
- **Respuesta superficial:** Decir "siempre se debe usar Hexagonal".
- **Lo que falta de fondo:** 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.
- **Respuesta senior:** 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.


## Katas

### Kata 27.1: Dominio Puro con Puerto de Salida y Adaptador JPA

Crea una interfaz `ProductoRepository` en `domain.port.out` (sin anotaciones Spring), una implementación `JpaProductoAdapter` en `infrastructure.adapter.out.persistence` que la implemente usando un `JpaRepository` de Spring Data, y un test unitario del servicio de dominio usando un mock del puerto.


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


## Proyecto guiado «Pedidos»: Arquitectura hexagonal

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

**Tareas:**

- Reorganiza el código en `dominio`, `aplicacion` e `infraestructura`.
- Define puertos de salida (`PedidoRepository`, `PublicadorEventos`) en el dominio y sus adaptadores en infraestructura.
- 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.


## Fuentes oficiales

- [Alistair Cockburn: Hexagonal Architecture](https://alistair.cockburn.us/hexagonal-architecture/)
- [Robert C. Martin: The Clean Architecture](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html)

