Skip to main content

Hexagonal Architecture: Ports and Adapters

Hexagonal Architecture isolates core business logic from external concerns using ports (interfaces) and adapters (implementations).

TL;DR

ConceptDefinition
Core IdeaApplication core isolated from external world
PortsInterfaces defining how core interacts with outside
AdaptersImplementations connecting ports to real systems
DependencyEverything 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);
}