Hexagonal (Ports & Adapters)

The domain defines ports (interfaces); each external actor (DB, UI, MQ) interacts through an adapter.

Context

Same goal as Clean: isolate the domain. The hexagonal metaphor emphasizes symmetry between what calls the domain (driving) and what the domain calls (driven).

Solution
  • Driving / Primary adapters: Web API, CLI, tests → call an input port of the domain.
  • Driven / Secondary adapters: EF, HTTP client, MQ → implement an output port of the domain.
       [HTTP API]   [CLI]   [Tests]
            │         │        │
            ▼         ▼        ▼
        ┌─────── Application ───────┐
        │  ports in   /  ports out  │
        └────────────┬──────────────┘
                     │
        ┌────────────┴──────────────┐
        ▼            ▼              ▼
    [EF Core]   [Stripe SDK]  [RabbitMQ]

#C# Example

// Input port (driving)
public interface IPlaceOrder { Task<Guid> Handle(PlaceOrderCommand cmd); }

// Output port (driven)
public interface IOrderRepository { void Add(Order o); }
public interface IPaymentGateway { Task<bool> ChargeAsync(decimal amount); }

// Use case (core, framework-agnostic)
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;
    }
}

// Concrete adapters live externally and are injected
Tradeoffs
Pro Con
Domain tests without bootstrapping anything Requires well-designed dual ports (in/out)
Freely swap adapters Careful initial design

#architecture #ports #adapters