Arquitectura Hexagonal en Java: lógica limpia sin dolor

Entrenador de Python en Línea para Principiantes

Aprende Python fácilmente sin sobrecargarte de teoría. Resuelve tareas prácticas con verificación automática, recibe pistas y escribe código directamente en el navegador, sin instalar nada.

Start Course

Introducción: por qué su aplicación no es su base de datos

\n\n

Imagine 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\n

Así 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\n

La 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\n

Hoy 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\n

La 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\n

Cada 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
\n\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
\n\n

Estructura del proyecto: cómo organizar el código

\n\n

No 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\n
com.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\n

Nota: domain no importa nada de adapter. Esto es crítico. Si ve @Entity o @RestController en domain, significa que la arquitectura está rota.

\n\n

Ejemplo: sistema de gestión de pedidos

\n\n

Diseñ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\n

Paso 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                

Blogs

Book Recommendations