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 injectedTradeoffs
| Pro | Con |
|---|---|
| Domain tests without bootstrapping anything | Requires well-designed dual ports (in/out) |
| Freely swap adapters | Careful initial design |
#architecture #ports #adapters