Hexagonal (Ports & Adapters)
El dominio define puertos (interfaces); cada actor externo (API, DB, cola, tests) llega a través de un adaptador intercambiable.
Propuesta por Alistair Cockburn en 2005. Nace de un problema concreto: la lógica de negocio terminaba atada a la UI y a la base de datos, y era imposible probarla sola. La idea central es que la aplicación no tiene un "arriba" y un "abajo", sino un adentro (el negocio) y un afuera (todo lo demás), y que todo lo de afuera entra y sale por puertos.
La lógica vive mezclada con el controller, el ORM o el cliente HTTP. Para probar una regla hay que levantar la base de datos; para cambiar de proveedor de pagos hay que tocar el caso de uso.
- Puerto: una interfaz definida por el núcleo, con el lenguaje del negocio.
- Adaptador: código que traduce entre una tecnología concreta y un puerto.
- Hay dos lados, y esa simetría es lo distintivo del patrón:
- Driving / primarios (quien llama a la aplicación): Web API, CLI, consumidor de mensajes, tests. Usan un puerto de entrada.
- Driven / secundarios (a quien la aplicación llama): base de datos, proveedor de pagos, email. Implementan un puerto de salida.
Driving (primarios) Driven (secundarios)
┌────────────────────┐ ┌─────────────────────┐
│ HTTP API │ │ EF Core │
│ CLI │──▶ [puerto IN] │ Stripe SDK │
│ Consumer de cola │ │ │ SMTP │
│ Tests │ ▼ └─────────▲───────────┘
└────────────────────┘ ┌──────────┐ │
│ NÚCLEO │─▶ [puerto OUT]
│ (negocio)│
└──────────┘
Regla de dependencia: los adaptadores dependen del núcleo; el núcleo no conoce a ningún adaptador.
#Por qué un hexágono
El hexágono no significa nada especial: Cockburn eligió una figura con varios lados para sugerir que puede haber muchos puertos, y para evitar el dibujo de capas "arriba/abajo". Lo importante es el concepto de adentro y afuera, no la forma.
#Ejemplo en C#
// Puerto de entrada (driving): lo que la aplicación ofrece
public interface IPlaceOrder { Task<Guid> Handle(PlaceOrderCommand cmd); }
// Puertos de salida (driven): lo que la aplicación necesita
public interface IOrderRepository { void Add(Order o); }
public interface IPaymentGateway { Task<bool> ChargeAsync(decimal amount); }
// Núcleo: sin frameworks, sin EF, sin HTTP
public class PlaceOrderUseCase : IPlaceOrder
{
private readonly IOrderRepository _repo;
private readonly IPaymentGateway _payments;
public PlaceOrderUseCase(IOrderRepository r, IPaymentGateway p) { _repo = r; _payments = p; }
public async Task<Guid> Handle(PlaceOrderCommand cmd)
{
var order = Order.Create(cmd);
if (!await _payments.ChargeAsync(order.Total))
throw new PaymentDeclined();
_repo.Add(order);
return order.Id;
}
}Los adaptadores viven afuera y se inyectan:
// Adaptador driving: traduce HTTP al puerto de entrada
app.MapPost("/orders", async (PlaceOrderRequest req, IPlaceOrder useCase) =>
Results.Ok(await useCase.Handle(new PlaceOrderCommand(req.CustomerId, req.Items))));
// Adaptador driven: traduce el puerto de salida a Stripe
public class StripePaymentGateway : IPaymentGateway
{
public async Task<bool> ChargeAsync(decimal amount) { /* llamada al SDK */ return true; }
}
#Tests: la ganancia más concreta
El puerto de salida se reemplaza por un fake en memoria: el caso de uso se prueba en milisegundos, sin base de datos ni red.
public class FakePaymentGateway : IPaymentGateway
{
public bool Approve { get; set; } = true;
public Task<bool> ChargeAsync(decimal amount) => Task.FromResult(Approve);
}
El mismo núcleo puede ejecutarse desde un test, una API o un worker: son todos adaptadores driving distintos.
#Errores comunes
- Puertos que filtran tecnología:
IOrderRepository.GetQueryable()devuelveIQueryablede EF. El puerto debe hablar el idioma del negocio, no el del ORM. - Una interfaz por clase sin motivo: un puerto existe para aislar algo que cambia o que molesta en los tests, no para tener interfaces de todo.
- Lógica en el adaptador: si el controller decide reglas de negocio, el hexágono está agujereado.
- Puerto de entrada ignorado: el controller llama directo al repositorio y se saltea el caso de uso.
#Hexagonal en relación con las otras
Hexagonal, Onion y Clean comparten la misma idea (dominio al centro, dependencias hacia adentro) y se diferencian en cuánto prescriben de la estructura interna. Hexagonal es la más liviana: solo dice qué está adentro y qué afuera, y no obliga a un número de capas internas. Ver la comparativa.
- CRUD sin reglas de negocio: los puertos solo agregan interfaces.
- Scripts, prototipos o microservicios de pocas líneas.
- Si no vas a escribir tests del dominio ni a cambiar de tecnología, el costo no se recupera.
| Pro | Contra |
|---|---|
| Tests del núcleo sin levantar nada | Interfaces y adaptadores adicionales |
| Intercambiás adaptadores (DB, proveedor, cola) | Hay que diseñar bien los puertos desde el inicio |
| Mismo núcleo para API, CLI y workers | Mapeos entre el modelo del adaptador y el del núcleo |
| La más liviana de las "dominio al centro" | No te dice cómo organizar el interior del núcleo |
#architecture #ports #adapters #hexagonal