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:
- DominioDominio
Entidad 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.
- PuertosPuertos
Trait PedidoRepositoryTrait NotificadorDePedidos - AdaptadoresAdaptadores
PostgresPedidoRepositorySmtpNotificadorDePedidosHandler HTTP de confirmación de pedidos - InfraestructuraInfraestructura
Axum (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á.
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:
/// 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í:
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:
- DominioDominio
Entidad 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.
- PuertosPuertos
Trait PedidoRepositoryTrait NotificadorDePedidos - AdaptadoresAdaptadores
PostgresPedidoRepositorySmtpNotificadorDePedidosHandler HTTP de confirmación de pedidos - InfraestructuraInfraestructura
Axum (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
- Usuario (arriba)
- Negocio (en medio)
- Base de datos (abajo)
Una sola entrada, una sola salida: el dibujo decide por ti.
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
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
- Alistair Cockburn, Hexagonal Architecture (HaT Technical Report, 2005): alistair.cockburn.us/hexagonal-architecture
- Juan Manuel Garrido de Paz, Ports and Adapters Architecture (Hexagonal Me): jmgarridopaz.github.io/content/hexagonalarchitecture.html
- Wikipedia, Hexagonal architecture (software) (el renombre a Ports and Adapters en 2005): en.wikipedia.org/wiki/Hexagonal_architecture_(software)