3. Conceptos fundamentales de la arquitectura hexagonal

La arquitectura hexagonal, también llamada de puertos y adaptadores, propone una distribución concéntrica del software, ubicando el dominio en el centro y conectándolo con el exterior por medio de contratos explícitos. Su objetivo es brindar independencia tecnológica, claridad de responsabilidades y un flujo de información controlado entre la aplicación y su entorno.

En este tema exploramos los elementos esenciales del modelo: la razón del hexágono, el papel del dominio y los casos de uso, la definición de puertos como interfaces, la implementación de adaptadores y la forma en que circulan los datos desde y hacia la aplicación.

3.1 ¿Qué significa “hexagonal” en este contexto?

El término “hexagonal” no implica una estructura estricta de seis lados en el código. Es una metáfora introducida por Alistair Cockburn en su artículo Hexagonal Architecture para representar múltiples puntos de interacción alrededor del dominio. Cada lado del hexágono simboliza un puerto potencial: una API, una interfaz gráfica, un servicio externo o cualquier otro canal que conecte el núcleo con el mundo exterior.

El gráfico del hexágono destaca que todos los lados son equivalentes: no hay una jerarquía que favorezca a la infraestructura sobre la lógica de negocio. Las entradas y salidas se organizan como adaptadores intercambiables que dependen del dominio, en lugar de que el dominio dependa de ellos. Este giro permite reemplazar tecnologías sin reescribir casos de uso.

3.2 El núcleo de la aplicación: dominio y casos de uso

En el centro de la arquitectura hexagonal se ubica el dominio, compuesto por entidades, reglas y lógica inmutable respecto a las necesidades del negocio. Los casos de uso orquestan estas reglas para resolver situaciones concretas, como registrar un pedido o calcular una cuota. Ambos elementos permanecen independientes de frameworks, bases de datos o interfaces gráficas.

Un caso de uso típico se expresa con clases sencillas que reciben interfaces como dependencias. Esto refuerza el lenguaje ubicuo del negocio y evita que el código del núcleo se contamine con detalles técnicos.

package com.example.ventas.dominio;

public class RegistrarPedido {

    private final PedidoRepositorio puertoRepositorio;
    private final NotificadorCliente puertoNotificador;

    public RegistrarPedido(PedidoRepositorio puertoRepositorio, NotificadorCliente puertoNotificador) {
        this.puertoRepositorio = puertoRepositorio;
        this.puertoNotificador = puertoNotificador;
    }

    public void ejecutar(Pedido pedido) {
        pedido.validar();
        puertoRepositorio.guardar(pedido);
        puertoNotificador.enviarConfirmacion(pedido);
    }
}

La clase RegistrarPedido sólo conoce contratos definidos en el dominio (PedidoRepositorio y NotificadorCliente) y manipula objetos propios de la capa central. No utiliza anotaciones específicas ni invoca APIs de terceros.

3.3 Los puertos: interfaces que definen contratos

Los puertos son interfaces declaradas dentro del dominio o la capa de aplicación que formalizan lo que el núcleo necesita o ofrece. Hay dos categorías principales:

  • Puertos de entrada: describen las operaciones que actores externos pueden solicitar. Suelen representarse como interfaces de casos de uso o controladores de aplicación.
  • Puertos de salida: detallan capacidades que el dominio requiere del exterior, como persistencia, mensajería o integración con terceros.

Los puertos permiten testear en aislamiento porque pueden implementarse con dobles de prueba (mocks, fakes o stubs). Además, funcionan como contratos explícitos entre equipos: quien desarrolla la infraestructura conoce exactamente qué métodos debe implementar y qué datos manipular.

3.4 Los adaptadores: implementaciones concretas para cada puerto

Cada puerto se materializa mediante uno o varios adaptadores. Un adaptador traduce la tecnología elegida al lenguaje del dominio. Por ejemplo, un adaptador de entrada podría ser un controlador REST que recibe solicitudes HTTP, mientras que uno de salida podría ser un repositorio JPA o un cliente de mensajería.

package com.example.ventas.adaptadores.salida.jpa;

import com.example.ventas.dominio.Pedido;
import com.example.ventas.dominio.PedidoRepositorio;
import org.springframework.stereotype.Repository;

@Repository
public class PedidoRepositorioJpa implements PedidoRepositorio {

    private final JpaPedidoRepository jpaRepository;

    public PedidoRepositorioJpa(JpaPedidoRepository jpaRepository) {
        this.jpaRepository = jpaRepository;
    }

    @Override
    public void guardar(Pedido pedido) {
        jpaRepository.save(PedidoEntity.fromDomain(pedido));
    }
}

El adaptador depende tanto del dominio (para convertir y aplicar las reglas) como de la infraestructura elegida (el repositorio JPA). Si mañana se decide usar MongoDB o un servicio externo, se crea un nuevo adaptador sin modificar la interfaz PedidoRepositorio.

3.5 Flujo general de la información en una aplicación hexagonal

El flujo inicia en un adaptador de entrada, por ejemplo una API REST, que recibe una petición y la convierte en un comando del dominio. El comando invoca un puerto de entrada, que a su vez delega el trabajo al caso de uso correspondiente. Durante la ejecución, el caso de uso utiliza puertos de salida para interactuar con bases de datos, colas o sistemas externos. Cada interacción se canaliza a través de adaptadores especializados.

La comunicación es bidireccional pero controlada. Ninguna dependencia cruza desde los adaptadores hacia el dominio sin pasar por un puerto definido. Este esquema garantiza que la lógica central pueda operar con distintos canales: una misma regla puede atender solicitudes vía REST, eventos asíncronos o interfaces de línea de comandos, siempre que existan adaptadores adecuados.

La siguiente secuencia resume el flujo típico:

  1. Un adaptador de entrada convierte la petición externa en un comando del dominio.
  2. El puerto de entrada invoca el caso de uso más apropiado.
  3. El caso de uso valida y procesa los datos, invocando puertos de salida según sea necesario.
  4. Adaptadores de salida ejecutan las operaciones concretas (persistir, enviar mensajes, consultar servicios).
  5. La respuesta del dominio vuelve a través del adaptador de entrada al consumidor original.

Este circuito permite incorporar nuevas tecnologías, refactorizar adaptadores o extender canales sin tocar la lógica central. Además, facilita la automatización de pruebas: el dominio puede evaluarse con adaptadores dobles y el comportamiento de cada puerto puede verificarse con pruebas de integración específicas.

3.6 Próximos pasos

Ahora que comprendemos los conceptos clave —dominio, puertos, adaptadores y flujo de información—, estamos en condiciones de organizar un proyecto concreto siguiendo esta arquitectura. En el próximo tema describiremos una estructura de carpetas y módulos recomendada para implementar estos principios en Java y otros lenguajes.