Saltar al contenido

Arquitectura hexagonal en la práctica: ports, adapters y cuándo usarlos

A
abemon
| | 12 min de lectura | Escrito por profesionales
Compartir

La teoría ya la conoces

Si estás leyendo esto, probablemente ya sabes qué es la arquitectura hexagonal. Alistair Cockburn la propuso en 2005 con un nombre más largo (Ports and Adapters) y una idea simple: tu lógica de negocio no debería saber nada del mundo exterior. Las bases de datos, las APIs, los frameworks web, la interfaz de usuario: todo eso son detalles de implementación que se conectan al núcleo a través de puertos (interfaces) y adaptadores (implementaciones).

La teoría es elegante. La práctica es donde las cosas se complican. Porque la pregunta que la teoría no responde es: cuándo merece la pena? Y la respuesta honesta es que en muchos proyectos, no merece la pena. Al menos no desde el principio. (Si buscas una introducción más amplia a decisiones de arquitectura, nuestro artículo sobre el coste real de la deuda técnica pone en contexto por qué estas decisiones importan.)

Cuándo es overkill

Vamos a ser directos. La arquitectura hexagonal añade complejidad estructural. Más archivos, más interfaces, más indirecciones. Para ciertos tipos de proyectos, esa complejidad no se justifica:

CRUD simples. Si tu aplicación es fundamentalmente un formulario conectado a una base de datos con validaciones básicas, la arquitectura hexagonal es un cañon para matar un mosquito. Un controller que llama a un servicio que llama a un repositorio es suficiente. Añadir puertos y adaptadores a una operación de “lee esto, valida aquello, guarda lo otro” es ingeniería sin propósito.

Prototipos y MVPs. Cuando no sabes si el producto va a sobrevivir tres meses, invertir en una arquitectura desacoplada es optimizar prematuramente. El objetivo del MVP es validar la hipótesis, no ganar premios de ingeniería. Si sobrevive, refactorizas. Si no, no has desperdiciado semanas en abstracciones.

Proyectos con un solo adaptador por puerto. Si tu aplicación tiene una base de datos PostgreSQL y siempre va a tener una base de datos PostgreSQL, crear una interfaz RepositoryPort y un adaptador PostgresAdapter es ceremonia sin beneficio. La interfaz solo tiene sentido si hay una posibilidad real de que necesites otra implementación (testing aparte, que trataremos después).

Entonces, cuándo sí tiene sentido?

Cuándo aporta valor real

Lógica de negocio compleja. Cuando tu dominio tiene reglas que no son triviales (cálculos de precios con descuentos escalonados, validaciones que dependen de estado, flujos de trabajo con múltiples condiciones), aislar esa lógica del framework y la infraestructura permite testearla, entenderla y modificarla sin riesgo. Hemos trabajado en un sistema de cotización logística donde la lógica de cálculo de precios dependía de 14 variables (un ejemplo de desarrollo a medida que justifica plenamente este enfoque) (peso, volumen, origen, destino, tipo de mercancía, urgencia, ruta, temporada, cliente, convenio…). Esa lógica vive en el dominio, completamente desacoplada. Podemos testear 200 combinaciones en 3 segundos sin levantar un servidor.

Múltiples canales de entrada. Si tu aplicación se consume a través de una API REST, un CLI, un sistema de colas, y un webhook, tener un único núcleo con múltiples adaptadores de entrada evita duplicar lógica. Cada canal es un adaptador que traduce su formato al lenguaje del dominio.

Integraciones cambiantes. Si tu aplicación se integra con servicios externos que pueden cambiar (migración de un CRM a otro, cambio de proveedor de pagos, nueva API de un partner), los adaptadores de salida te permiten hacer el cambio sin tocar la lógica de negocio. Esto no es teórico: hemos migrado un cliente de un ERP a otro cambiando un adaptador sin modificar ni una línea del dominio. (Más sobre patrones de integración ERP en nuestro executive brief dedicado.)

Equipos grandes. Cuando hay 8 o 10 desarrolladores trabajando en el mismo sistema, la arquitectura hexagonal establece fronteras claras. El equipo de dominio no necesita saber cómo funciona el adaptador de Kafka. El equipo de infraestructura no necesita entender las reglas de negocio. Las interfaces (puertos) son el contrato entre ambos.

Refactorización incremental

La forma más habitual de llegar a la arquitectura hexagonal no es empezar desde cero. Es refactorizar un sistema existente que ha crecido hasta un punto donde el acoplamiento causa dolor real: tests que requieren base de datos, cambios en la API que rompen lógica de negocio, o integraciones que se extienden por toda la aplicación como enredaderas.

El proceso incremental que hemos usado en proyectos reales:

Paso 1: Extraer el dominio. Identifica la lógica de negocio que está mezclada con infraestructura. Muévela a clases o módulos puros, sin dependencias de framework, base de datos, o librerías externas. No cambies la lógica. Solo muévela. Esto ya es útil por sí mismo, porque puedes empezar a testearla de forma aislada.

Paso 2: Definir puertos de salida. Para cada dependencia externa que usa tu dominio (base de datos, API externa, sistema de archivos, cola de mensajes), crea una interfaz que defina qué necesita el dominio. No qué ofrece la infraestructura, sino qué necesita el dominio. La diferencia es crítica. Tu dominio no necesita “un método findAll que devuelve una lista paginada con ordenación”; necesita “obtener las facturas pendientes de un cliente.” El puerto refleja el lenguaje del dominio, no el de la base de datos.

Paso 3: Implementar adaptadores. Cada implementación concreta de un puerto es un adaptador. Tu PostgresInvoiceRepository implementa InvoiceRepository (el puerto). Tu StripePaymentGateway implementa PaymentGateway. Los adaptadores viven fuera del dominio, en una capa de infraestructura.

Paso 4: Invertir dependencias. Ahora el dominio depende solo de interfaces que él mismo define. La infraestructura depende del dominio (implementa sus interfaces), no al revés. Esto es Dependency Inversion (la D de SOLID) aplicada a nivel arquitectónico. En la práctica, necesitas un mecanismo de inyección de dependencias. En Spring, lo tienes nativo. En Python, FastAPI tiene Depends. En Node, puedes usar un contenedor simple o composición manual.

Paso 5: Definir puertos de entrada. Opcional y muchas veces innecesario en la primera iteración. Los puertos de entrada (use cases o application services) definen las operaciones que el mundo exterior puede invocar. Son útiles cuando tienes múltiples puntos de entrada o cuando quieres documentar formalmente la API de tu dominio. Si tu único punto de entrada es un controller REST, un servicio de aplicación que delega al dominio es suficiente.

Estructura real de proyecto

La teoría dice “hexagonal”. Los directorios del proyecto dicen otra cosa. Esta es una estructura que hemos usado en producción con Python (FastAPI) y que funciona para equipos de 3 a 8 personas:

src/
  domain/
    models/          # Entidades y value objects
    ports/           # Interfaces de salida (repositories, gateways)
    services/        # Logica de negocio pura
    exceptions/      # Excepciones de dominio
  application/
    use_cases/       # Orquestacion de logica de dominio
    dto/             # Data transfer objects
  infrastructure/
    adapters/
      persistence/   # Implementaciones de repositorios
      external/      # Clientes de APIs externas
      messaging/     # Productores y consumidores de colas
    web/
      controllers/   # Endpoints REST
      middleware/     # Autenticacion, logging, etc.
    config/          # Inyeccion de dependencias, configuracion

Lo que importa no son los nombres de los directorios (puedes llamarlos como quieras) sino las reglas de dependencia:

  • domain/ no importa nada de infrastructure/ ni de application/
  • application/ importa de domain/ pero no de infrastructure/
  • infrastructure/ importa de domain/ y application/

Si tu IDE tiene análisis de dependencias (IntelliJ lo tiene, hay plugins para VS Code), configura estas reglas y haz que el CI las verifique. Una regla de dependencia violada hoy es deuda técnica mañana.

Testing: donde brilla la arquitectura hexagonal

Si hay un argumento definitivo a favor de esta arquitectura, es la testabilidad. La lógica de dominio, al no depender de infraestructura, se puede testear con tests unitarios rápidos y deterministas.

Tests de dominio. Puros, rápidos, sin dependencias externas. Testean reglas de negocio con datos de entrada conocidos y verifican resultados. 500 tests en 2 segundos. Sin base de datos, sin servidor, sin red.

Tests de adaptadores. Testean que los adaptadores implementan correctamente los puertos. El adaptador de persistencia se testea con una base de datos de test (o con testcontainers). El adaptador de API externa se testea con un mock del servicio. Estos tests son más lentos pero necesarios para verificar integración.

Tests de aceptación. Testean el sistema completo, de punta a punta. Un request HTTP que recorre controller, use case, dominio, adaptador, base de datos, y vuelve. Pocos, lentos, y centrados en los flujos críticos de negocio.

La pirámide de testing clásica (muchos unitarios, algunos de integración, pocos e2e) encaja naturalmente con esta arquitectura. Sin ella, todos tus tests necesitan infraestructura y son lentos. Con ella, el 80% de tus tests son unitarios puros que corren en milisegundos.

Errores comunes

Después de implementar (y ayudar a implementar) está arquitectura en una docena de proyectos, hay errores que se repiten:

Sobre-abstraer. Crear interfaces para todo, incluyendo lógica interna del dominio que nunca va a tener otra implementación. Los puertos son fronteras con el exterior. Dentro del dominio, la simplicidad gana.

Anemia de dominio. Poner toda la lógica en use cases y dejar las entidades como simples contenedores de datos. Si tu entidad Invoice no sabe calcular su total ni validar su consistencia, tu dominio está anémico y los beneficios de la arquitectura se diluyen.

Puertos que reflejan la infraestructura. Un puerto findAllWithPaginationAndSortBy es un repositorio disfrazado de interfaz. El puerto debe hablar el lenguaje del dominio: getOutstandingInvoicesForClient, findOverduePayments.

No invertir las dependencias. Mover código a carpetas con nombres bonitos pero mantener imports directos de la base de datos en el dominio. Sin inversión de dependencias real, la arquitectura hexagonal es cosmética.

Adapters que contienen lógica de negocio. El adaptador de persistencia que calcula descuentos antes de guardar una factura. El adaptador de API externa que decide si reintentar basándose en reglas de negocio. Los adaptadores son traductores, no decisores. Si tu adaptador tiene un if que depende de una regla de negocio, esa lógica debería estar en el dominio.

Hexagonal vs Clean Architecture vs Onion

Vale la pena aclarar la relación entre estas tres arquitecturas, porque la confusión es frecuente. Son variaciones del mismo principio (dependencias apuntando hacia el dominio) con diferencias de nomenclatura más que de sustancia.

Hexagonal (Cockburn, 2005) habla de puertos y adaptadores. El énfasis está en que el dominio es agnóstico respecto al mundo exterior, y el mundo exterior se conecta a través de interfaces.

Clean Architecture (Martin, 2012) añade más capas (entities, use cases, interface adapters, frameworks) y formaliza la “regla de dependencia”: las capas internas no conocen a las externas. En la práctica, la implementación es muy similar a hexagonal con la capa de application separada.

Onion Architecture (Palermo, 2008) usa la metáfora de capas concentricas. El núcleo es el dominio, las capas exteriores son infraestructura. Misma idea, diferente diagrama.

Para proyectos reales, la elección entre las tres es irrelevante. Lo que importa es el principio: el dominio no depende de la infraestructura. Si tu equipo entiende ese principio, da igual cómo lo llames. Si no lo entiende, ningún nombre lo va a arreglar.

En nuestra práctica, usamos terminología hexagonal (puertos y adaptadores) porque es la más concreta. “Puerto” es más claro que “capa de interface adapters.” Pero si tu equipo viene de Clean Architecture y esa terminología les resulta familiar, úsala. La pelea por nombres es la pelea menos productiva que puede tener un equipo de ingeniería.

Cuándo empezar

Si estás empezando un proyecto nuevo y sabes que la lógica de negocio va a ser compleja, empieza con hexagonal desde el día uno. El coste adicional en la fase inicial es modesto (unas horas de setup) y el beneficio a largo plazo es enorme.

Si tienes un proyecto existente con dolor de acoplamiento, empieza por el Paso 1: extraer el dominio de un módulo concreto. No intentes refactorizar todo el sistema a la vez. Elige el módulo con más reglas de negocio, aíslalo, y usa ese como modelo para el resto. La refactorización incremental funciona. La refactorización big-bang, en nuestra experiencia, no.

La arquitectura hexagonal no es un fin en sí misma. Es una herramienta para mantener la lógica de negocio comprensible, testeable y modificable a medida que el sistema crece. Si tu sistema no va a crecer mucho, probablemente no la necesites. Si va a crecer (y la mayoría de los sistemas que sobreviven, crecen), es la mejor inversión de ingeniería que puedes hacer.

Sobre el autor

A

abemon engineering

Equipo de ingenieria

Equipo multidisciplinar de ingenieria, datos e IA con sede en Canarias. Construimos, desplegamos y operamos soluciones de software a medida para empresas de cualquier escala.

Síguenos: LinkedIn GitHub