Hexagonal (Ports & Adapters)

El dominio define puertos (interfaces); cada actor externo (API, DB, cola, tests) llega a través de un adaptador intercambiable.

Contexto

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.

Problema

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.

Solución
  • 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:

CSHARP
// 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.

CSHARP
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() devuelve IQueryable de 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.

Cuándo NO aplicarlo
  • 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.
Tradeoffs
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