Introducción: por qué su aplicación no es su base de datos
\n\nImagine que está construyendo una casa. Cimientos, paredes, techo: todo está pensado. Pero al año decide cambiar el cableado. Y resulta que los cables están empotrados en el hormigón y los enchufes soldados a las varillas. Para cambiar un solo interruptor, hay que derribar media casa.
\n\nAsí es más o menos una aplicación Java clásica, donde la lógica de negocio está estrechamente entrelazada con el framework, la base de datos o el controlador REST. ¿Quiere migrar de PostgreSQL a MongoDB? ¿Reescribir los controladores de Spring MVC a JAX-RS? Bienvenido al infierno de la refactorización.
\n\nLa solución es la Arquitectura Hexagonal (también conocida como «Puertos y Adaptadores», o «Arquitectura Limpia» en la interpretación de Alistair Cockburn). No es una bala de plata, pero es una forma de hacer que su aplicación sea flexible, testeable e independiente del mundo exterior.
\n\nHoy analizaremos la Arquitectura Hexagonal en Java con ejemplos reales de código, sin rodeos ni reflexiones abstractas. ¡Allá vamos!
\n\n¿Qué es la Arquitectura Hexagonal (Puertos y Adaptadores)?
\n\nLa esencia es simple: su aplicación es un núcleo (core) que contiene la lógica de negocio. Alrededor del núcleo hay un «hexágono» (en realidad, un número arbitrario de caras) a través del cual el mundo exterior se comunica con el núcleo.
\n\nCada cara tiene su propio puerto (interfaz). Y un adaptador es la implementación del puerto para una tecnología específica. Por ejemplo:
\n- \n
- Puerto «UserRepository» — adaptador «JpaUserRepository» o «MongoUserRepository» \n
- Puerto «SendNotification» — adaptador «EmailSender» o «SmsSender» \n
La regla principal: el núcleo no sabe nada sobre bibliotecas externas, bases de datos o frameworks web. Solo conoce sus puertos (interfaces). Esto proporciona varias ventajas:
\n- \n
- Independencia de la infraestructura — cambie la BD o el framework sin tocar la lógica. \n
- Testeabilidad — la lógica de negocio se puede probar sin levantar el contexto de Spring ni la base de datos. \n
- Legibilidad — el código del núcleo cuenta la historia del dominio, no los detalles técnicos. \n
Estructura del proyecto: cómo organizar el código
\n\nNo existe una única estructura de paquetes correcta, pero propongo una opción probada basada en DDD (Domain-Driven Design) y los principios de Hexagonal:
\n\ncom.example.app\n├── domain\n│ ├── model // Entidades, Value Objects\n│ ├── port // Interfaces de puertos (inbound/outbound)\n│ └── service // Servicios de negocio (casos de uso)\n├── adapter\n│ ├── inbound\n│ │ ├── rest // Controladores REST (adaptadores entrantes)\n│ │ └── messaging // Escuchas Kafka/RabbitMQ\n│ └── outbound\n│ ├── persistence // Repositorios JPA, JDBC, MongoDB\n│ └── client // Clientes HTTP, APIs externas\n└── config // Configuración de Spring (si se usa)\n\n\nNota: domain no importa nada de adapter. Esto es crítico. Si ve @Entity o @RestController en domain, significa que la arquitectura está rota.
Ejemplo: sistema de gestión de pedidos
\n\nDiseñemos un sistema simple: un usuario puede crear un pedido, y el sistema verifica la disponibilidad del producto en el almacén y envía una notificación.
\n\nPaso 1. Modelo de dominio (domain/model)
\n\n// Order.java\npublic class Order {\n private final OrderId id;\n private final List<OrderItem> items;\n private OrderStatus status;\n\n public Order(List<OrderItem> items) {\n this.id = new OrderId(UUID.randomUUID().toString());\n this.items = Collections.unmodifiableList(items);\n this.status = OrderStatus.PENDING;\n }\n\n public void confirm() {\n if (status != OrderStatus.PENDING) {\n throw new IllegalStateException("Only PENDING orders can be confirmed");\n }\n this.status = Ord