Hexagonal Architecture: Ports and Adapters
Hexagonal Architecture isolates core business logic from external concerns using ports (interfaces) and adapters (implementations).
TL;DR
| Concept | Definition |
|---|---|
| Core Idea | Application core isolated from external world |
| Ports | Interfaces defining how core interacts with outside |
| Adapters | Implementations connecting ports to real systems |
| Dependency | Everything depends on core, never reverse |
Ports (Interfaces)
// Driving Port - how outside world calls app
public interface IOrderService
{
Task<Order> CreateOrder(CreateOrderCommand command);
Task<Order> GetOrder(OrderId orderId);
}
// Driven Port - how app calls external systems
public interface IOrderRepository
{
Task<Order?> GetById(OrderId id);
Task Save(Order order);
}
public interface IPaymentGateway
{
Task<PaymentResult> ProcessPayment(PaymentRequest request);
}
Adapters (Implementations)
// Driving Adapter - REST API
[ApiController]
public class OrdersController : ControllerBase
{
private readonly IOrderService _orderService;
[HttpPost]
public async Task<ActionResult<OrderDto>> CreateOrder(CreateOrderRequest request)
{
var order = await _orderService.CreateOrder(MapToCommand(request));
return Ok(MapToDto(order));
}
}
// Driven Adapter - Database
public class SqlOrderRepository : IOrderRepository
{
public async Task<Order?> GetById(OrderId id)
{
var entity = await _context.Orders.FindAsync(id.Value);
return entity != null ? MapToDomain(entity) : null;
}
}
// Driven Adapter - Payment
public class StripePaymentGateway : IPaymentGateway
{
public async Task<PaymentResult> ProcessPayment(PaymentRequest request)
{
var intent = await _stripe.PaymentIntents.CreateAsync(...);
return new PaymentResult(intent.Id, intent.Status == "succeeded");
}
}
Project Structure
src/
├── Core/ # Application Core
│ ├── Domain/ # Entities, Value Objects
│ ├── Ports/
│ │ ├── Driving/ # IOrderService
│ │ └── Driven/ # IOrderRepository
│ └── Services/ # OrderService
├── Adapters/
│ ├── Driving/ # Controllers, CLI
│ └── Driven/ # Repositories, API clients
└── Host/ # Composition root
Common Mistakes
// ❌ Domain with EF attributes
public class Order
{
[Key] public Guid Id { get; set; }
}
// ✅ Pure domain
public class Order
{
public OrderId Id { get; }
}
// ❌ Leaky port
public interface IOrderRepository
{
IQueryable<Order> Query(); // Exposes EF
}
// ✅ Clean port
public interface IOrderRepository
{
Task<Order?> GetById(OrderId id);
}