Saltar al contenido
Módulo 2 de 7Básico

El hexágono: puertos, adaptadores y el mito de los seis lados

Las tres piezas del patrón con precisión quirúrgica: el núcleo que ignora la tecnología, los puertos que definen conversaciones y los adaptadores que las traducen — y por qué el hexágono no tiene nada que ver con el número seis.

En el módulo anterior dejamos a la cafetería con una regla de descuento rescatada de entre consultas SQL y peticiones HTTP, y con una pregunta pendiente: si el Pedido ya no conoce la base de datos, ¿quién guarda los pedidos? ¿Quién avisa al cliente cuando el suyo está listo? Hoy respondemos con las tres piezas del patrón —núcleo, puertos y adaptadores— y de paso desmontamos el malentendido más famoso de la arquitectura hexagonal: el de los seis lados.

Imagina el dibujo en una servilleta: un hexágono en el centro de la hoja. Adentro, todo lo que hace que la cafetería sea la cafetería: sus pedidos, sus reglas, sus casos de uso. Afuera, todo lo que hoy es una tecnología y mañana puede ser otra: la web, la base de datos, el correo. Y sobre el borde del hexágono, la parte más importante del dibujo: los puntos de conversación entre ambos mundos.

Ese dibujo ya existe aquí, con las piezas de la cafetería en su sitio. Explora cada anillo antes de que lo recorramos pieza por pieza:

  • DominioDominioEntidad PedidoCaso de uso ConfirmarPedidoRegla «un pedido sin líneas no se confirma»

    Las cosas importantes para el problema de negocio: entidades, reglas y casos de uso, sin referencias a ninguna tecnología, framework o dispositivo del mundo real.

  • PuertosPuertosTrait PedidoRepositoryTrait NotificadorDePedidos
  • AdaptadoresAdaptadoresPostgresPedidoRepositorySmtpNotificadorDePedidosHandler HTTP de confirmación de pedidos
  • InfraestructuraInfraestructuraAxum (framework web)Servidor PostgreSQL

Adentro: el núcleo

El criterio de admisión del interior es estricto. En palabras de Juan Manuel Garrido de Paz, colaborador de Cockburn en su libro de 2024:

“Inside the hexagon we just have the things that are important for the business problem.”

Es decir, la lógica de negocio “sin referencias a ninguna tecnología, framework o dispositivo del mundo real”. Para la cafetería, eso incluye la entidad Pedido con sus reglas, y también el caso de uso ConfirmarPedido: la operación completa que un actor le puede pedir a la aplicación. Confirmar un pedido es negocio puro —verificar que tenga líneas, cambiar su estado, disparar el aviso— aunque todavía no sepamos en qué base de datos se guardará.

dominio/pedido.rs — la entidad y su reglaDominio
pub enum EstadoPedido {
    Abierto,
    Confirmado,
}

pub struct Pedido {
    estado: EstadoPedido,
    lineas: Vec<LineaDePedido>,
}

impl Pedido {
    /// Regla de negocio: un pedido sin líneas no puede confirmarse.
    pub fn confirmar(&mut self) -> Result<(), ErrorDePedido> {
        if self.lineas.is_empty() {
            return Err(ErrorDePedido::PedidoVacio);
        }
        self.estado = EstadoPedido::Confirmado;
        Ok(())
    }
}

Ni un solo import tecnológico. Esta entidad compila y se prueba sin saber que existen la web o PostgreSQL. Pero para completar el caso de uso, el núcleo necesita cosas del exterior: recuperar el pedido de algún lugar, guardarlo confirmado, avisarle a alguien. ¿Cómo las pide sin acoplarse? Con puertos.

Los puertos: contratos con propósito

Un puerto es un contrato que define una conversación en la frontera de la aplicación. Garrido de Paz lo formula así: los puertos representan “the application boundary” y son “interfaces that the application offers to the outside world for allowing actors interact with the application”.

En Rust, un puerto es un trait. Estos son los dos que el caso de uso ConfirmarPedido necesita:

dominio/puertos.rs — lo que el núcleo necesitaPuertos
/// Puerto: guardar y recuperar pedidos, sin decir dónde.
pub trait PedidoRepository {
    fn buscar(&self, id: PedidoId) -> Result<Pedido, ErrorDeRepositorio>;
    fn guardar(&self, pedido: &Pedido) -> Result<(), ErrorDeRepositorio>;
}

/// Puerto: avisar que algo pasó con un pedido, sin decir cómo.
pub trait NotificadorDePedidos {
    fn pedido_confirmado(&self, pedido: &Pedido) -> Result<(), ErrorDeNotificacion>;
}

Observa los nombres, porque ahí vive una regla no negociable del patrón. Los puertos se nombran, cita textual, “according to what they are for, not according to any technology”: por su propósito, nunca por la tecnología. PedidoRepository dice para qué sirve (guardar y recuperar pedidos); NotificadorDePedidos dice para qué sirve (avisar sobre pedidos). Nombres como EnviarEmailSmtp, PedidoPostgresDAO o ClienteHttpDePagos delatan una tecnología concreta y convierten el contrato en una promesa rota: si el nombre ya dice “SMTP”, cambiar a WhatsApp te obliga a renombrar medio dominio.

Fíjate también en quién es el dueño: los puertos los define el núcleo, en su propio idioma y con sus propios tipos. El trait PedidoRepository habla de Pedido y PedidoId, no de filas ni de tablas. El exterior tendrá que adaptarse a este contrato, y no al revés.

Los adaptadores: la tecnología en su sitio

Del otro lado de la frontera viven los adaptadores. Cockburn los describe en el artículo original:

“For each external device there is an adapter that converts the API definition to the signals needed by that device and vice versa.”

Un adaptador es la implementación de un puerto para una tecnología específica: traduce la conversación agnóstica del contrato al dialecto concreto de un dispositivo, y viceversa. Para el puerto PedidoRepository, un adaptador Postgres se ve así:

adaptadores/postgres_pedidos.rs — el traductorAdaptadores
pub struct PostgresPedidoRepository {
    pool: PgPool,
}

impl PedidoRepository for PostgresPedidoRepository {
    fn guardar(&self, pedido: &Pedido) -> Result<(), ErrorDeRepositorio> {
        // Traduce la llamada agnóstica del puerto a un INSERT concreto.
        // Los tipos de sqlx y las tablas viven aquí, no en el dominio.
        // ...
    }

    fn buscar(&self, id: PedidoId) -> Result<Pedido, ErrorDeRepositorio> {
        // SELECT ... y reconstrucción del Pedido del dominio.
        // ...
    }
}

La gracia está en la intercambiabilidad: mañana escribes un SqlitePedidoRepository para desarrollo local o un repositorio en memoria para los tests, y el núcleo ni se entera, porque todos implementan el mismo trait. Lo mismo aplica al otro puerto: un SmtpNotificadorDePedidos hoy, uno de WhatsApp mañana, uno silencioso para los tests. Y nota la diferencia con los puertos: el adaptador sí puede llevar la tecnología en el nombre, porque su trabajo es precisamente ser concreto.

Un matiz que retomaremos en el próximo módulo: no todos los adaptadores implementan un puerto. El handler HTTP que recibe el POST /pedidos/42/confirmar también es un adaptador —traduce una petición web en una llamada al caso de uso— pero está del lado de quien llama a la aplicación, no del lado de quien le responde.

La regla de la frontera

Todo el patrón se sostiene sobre una sola regla, y Cockburn la enuncia sin rodeos:

“The rule to obey is that code pertaining to the inside part should not leak into the outside part.”

El código del interior no se filtra hacia afuera. Y la lectura inversa es igual de importante: nada del exterior —tipos del ORM, DTOs del framework, códigos de estado HTTP, excepciones del driver— cruza hacia adentro. Cada vez que un struct del dominio deriva un trait de serialización del framework web “porque era más cómodo”, o un caso de uso devuelve un 404, la frontera tiene una grieta. Las grietas no duelen el día que se abren; duelen el día que quieres cambiar de tecnología o probar el núcleo en aislamiento y descubres que ya no puedes.

¿Qué flechas pueden cruzar la frontera y cuáles no? Vuelve al mapa de los anillos, ahora con las reglas dibujadas; toca cada flecha para leer su motivo:

  • DominioDominioEntidad PedidoCaso de uso ConfirmarPedidoRegla «un pedido sin líneas no se confirma»

    Las cosas importantes para el problema de negocio: entidades, reglas y casos de uso, sin referencias a ninguna tecnología, framework o dispositivo del mundo real.

  • PuertosPuertosTrait PedidoRepositoryTrait NotificadorDePedidos
  • AdaptadoresAdaptadoresPostgresPedidoRepositorySmtpNotificadorDePedidosHandler HTTP de confirmación de pedidos
  • InfraestructuraInfraestructuraAxum (framework web)Servidor PostgreSQL
  • Adaptadores → Dominio — prohibida: una entidad del dominio no deriva un trait del framework
  • Adaptadores → Puertos — permitida: un adaptador implementa el trait del puerto
  • Dominio → Puertos — permitida: el dominio define y usa su propio puerto
  • Infraestructura → Adaptadores — permitida: axum y PostgreSQL son la tecnología detrás del adaptador

La frontera también te dice dónde está parada cada pieza. Pregúntale a cualquier fragmento de código dos cosas: ¿hablas el idioma del negocio o el de una tecnología? y ¿de qué lado de los puertos estás? Con esas dos preguntas —y las flechas que acabas de recorrer— puedes clasificar el sistema completo de la cafetería.

El mito de los seis lados

Y entonces, ¿por qué un hexágono? ¿Seis lados significan seis puertos, seis adaptadores, seis capas? Nada de eso. Cockburn lo aclaró él mismo:

“The hexagon is not a hexagon because the number six is important, but rather to allow the people doing the drawing to have room to insert ports and adapters as they need.”

Compara los dos dibujos que competían en las servilletas:

El rectángulo de siempre

  1. Usuario (arriba)
  2. Negocio (en medio)
  3. Base de datos (abajo)

Una sola entrada, una sola salida: el dibujo decide por ti.

Capas — el dibujo impone un flujo fijo

La servilleta de Cockburn

  • Puerto: la tablet de la barista
  • Puerto: los pedidos de la app de delivery
  • Puerto: guardar y recuperar pedidos
  • …y espacio libre para los que hagan falta
Ports & Adapters — lados de sobra

Cockburn necesitaba una forma que rompiera la inercia del rectángulo y dejara espacio para dibujar tantos puertos como hicieran falta, en cualquier posición. Un hexágono era simplemente eso: una figura no rectangular con lados de sobra. El número seis no tiene ningún significado técnico. Si tu aplicación necesita dos puertos, dibuja dos; si necesita nueve, dibuja nueve.

De hecho, el propio Cockburn renombró el patrón en 2005 como Ports and Adapters, porque ese nombre sí captura la esencia: lo importante son los contratos y sus traductores, no la figura geométrica. La historia quiso que “hexagonal” fuera el nombre que se popularizó, y por eso hoy desmontamos su mito en un curso que se llama, cómo no, hexagonal.

Minijuego: coloca cada pieza en su capa

El sistema de pedidos de la cafetería está desarmado sobre la mesa. Toma cada pieza y colócala en su anillo: dominio, puertos, adaptadores o infraestructura. Antes de decidir, hazle a cada una las dos preguntas de la frontera: ¿qué idioma habla? ¿de qué lado del contrato está?

Lo que viene

Ya tienes el mapa completo: un núcleo que ignora la tecnología, puertos nombrados por propósito y adaptadores que traducen. Pero quedó una asimetría pendiente: el handler HTTP usa al caso de uso, mientras que el repositorio Postgres es usado por él. Esa diferencia de dirección —quién inicia la conversación— es la distinción entre driving y driven, y es el tema del próximo módulo. Spoiler: no es lo mismo que “entrada y salida”, y confundirlas es el error más común del patrón.

Fuentes

Práctica

Practica lo aprendido

Tarjetas de repaso

Evaluación del módulo