Domain-Driven Hexagonal Architecture
A comprehensive guide to combining Domain-Driven Design (DDD) principles with Hexagonal (Ports and Adapters) Architecture for building maintainable, scalable applications.
Architecture Overview
The hexagonal architecture isolates the domain core from external concerns through ports (interfaces) and adapters (implementations).
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Infrastructure Layer β
β βββββββββββββββ βββββββββββββββ βββββββββββββββββββββββ β
β β Controllers β β Repositoriesβ β External Services β β
β β (Driving) β β (Driven) β β (Driven) β β
β ββββββββ¬βββββββ ββββββββ¬βββββββ ββββββββββββ¬βββββββββββ β
β β β β β
β ββββββββΌβββββββ ββββββββΌβββββββ ββββββββββββΌβββββββββββ β
β β Input Ports β βOutput Ports β β Output Ports β β
β β (Interfaces)β β(Interfaces) β β (Interfaces) β β
β ββββββββ¬βββββββ ββββββββ¬βββββββ ββββββββββββ¬βββββββββββ β
β β β β β
β ββββββββΌβββββββββββββββββΌββββββββββββββββββββββΌβββββββββββ β
β β Application Layer β β
β β βββββββββββββββββββ βββββββββββββββββββββββββββββββ β β
β β β Command Handlersβ β Query Handlers β β β
β β β (Use Cases) β β (Read Models) β β β
β β ββββββββββ¬βββββββββ ββββββββββββββββ¬βββββββββββββββ β β
β β β β β β
β β ββββββββββΌβββββββββββββββββββββββββββΌβββββββββββββββ β β
β β β Domain Layer (Core) β β β
β β β βββββββββββββββ βββββββββββββββ βββββββββββββ β β β
β β β β Entities β β Value β β Domain β β β β
β β β β Aggregates β β Objects β β Events β β β β
β β β βββββββββββββββ βββββββββββββββ βββββββββββββ β β β
β β β βββββββββββββββ βββββββββββββββ βββββββββββββ β β β
β β β β Domain β β Domain β βSpecifica- β β β β
β β β β Services β β Policies β β tions β β β β
β β β βββββββββββββββ βββββββββββββββ βββββββββββββ β β β
β β βββββββββββββββββββββββββββββββββββββββββββββββββββββ β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
DDD Building Blocks
Entities
Objects with unique identity that persists over time.
public class Order : Entity<OrderId>
{
public CustomerId CustomerId { get; private set; }
public OrderStatus Status { get; private set; }
private readonly List<OrderItem> _items = new();
public IReadOnlyList<OrderItem> Items => _items.AsReadOnly();
public void AddItem(Product product, int quantity)
{
if (Status != OrderStatus.Draft)
throw new DomainException("Cannot modify confirmed order");
var item = new OrderItem(product.Id, product.Price, quantity);
_items.Add(item);
AddDomainEvent(new OrderItemAddedEvent(Id, item));
}
}
Value Objects
Immutable objects defined by their attributes.
public record Money(decimal Amount, string Currency)
{
public static Money operator +(Money a, Money b)
{
if (a.Currency != b.Currency)
throw new DomainException("Currency mismatch");
return new Money(a.Amount + b.Amount, a.Currency);
}
}
public record Address(string Street, string City, string PostalCode, string Country);
Aggregates
Cluster of entities and value objects with a root entity.
public class OrderAggregate : AggregateRoot<OrderId>
{
public Customer Customer { get; private set; }
public ShippingAddress ShippingAddress { get; private set; }
public Money TotalAmount => CalculateTotal();
// Only the aggregate root can be referenced externally
// Internal entities accessed through root methods
}
Domain Events
Capture something that happened in the domain.
public record OrderPlacedEvent(OrderId OrderId, CustomerId CustomerId, Money Total)
: IDomainEvent;
public record PaymentReceivedEvent(OrderId OrderId, PaymentId PaymentId)
: IDomainEvent;
Domain Services
Operations that donβt naturally fit in an entity.
public class PricingService : IDomainService
{
public Money CalculatePrice(Order order, Customer customer)
{
var basePrice = order.Items.Sum(i => i.Price * i.Quantity);
var discount = customer.LoyaltyLevel.GetDiscount();
return basePrice * (1 - discount);
}
}
Hexagonal Architecture Implementation
Input Ports (Primary/Driving)
// Application service interface
public interface IOrderService
{
Task<OrderDto> PlaceOrderAsync(PlaceOrderCommand command);
Task<OrderDto> GetOrderAsync(OrderId orderId);
}
Output Ports (Secondary/Driven)
// Repository interface (defined in domain, implemented in infrastructure)
public interface IOrderRepository
{
Task<Order?> GetByIdAsync(OrderId id);
Task SaveAsync(Order order);
}
// External service interface
public interface IPaymentGateway
{
Task<PaymentResult> ProcessPaymentAsync(PaymentRequest request);
}
Input Adapters (Controllers)
[ApiController]
[Route("api/[controller]")]
public class OrdersController : ControllerBase
{
private readonly IOrderService _orderService;
[HttpPost]
public async Task<ActionResult<OrderDto>> PlaceOrder(PlaceOrderRequest request)
{
var command = new PlaceOrderCommand(request.CustomerId, request.Items);
var order = await _orderService.PlaceOrderAsync(command);
return CreatedAtAction(nameof(GetOrder), new { id = order.Id }, order);
}
}
Output Adapters (Implementations)
// Repository implementation
public class EfOrderRepository : IOrderRepository
{
private readonly AppDbContext _context;
public async Task<Order?> GetByIdAsync(OrderId id)
{
return await _context.Orders
.Include(o => o.Items)
.FirstOrDefaultAsync(o => o.Id == id);
}
}
// External service adapter
public class StripePaymentGateway : IPaymentGateway
{
private readonly StripeClient _client;
public async Task<PaymentResult> ProcessPaymentAsync(PaymentRequest request)
{
var charge = await _client.Charges.CreateAsync(new ChargeCreateOptions
{
Amount = (long)(request.Amount * 100),
Currency = request.Currency,
Source = request.Token
});
return new PaymentResult(charge.Id, charge.Status == "succeeded");
}
}
Project Structure
src/
βββ Domain/
β βββ Entities/
β β βββ Order.cs
β β βββ Customer.cs
β βββ ValueObjects/
β β βββ Money.cs
β β βββ Address.cs
β βββ Aggregates/
β β βββ OrderAggregate.cs
β βββ Events/
β β βββ OrderPlacedEvent.cs
β βββ Services/
β β βββ PricingService.cs
β βββ Ports/
β βββ IOrderRepository.cs
β βββ IPaymentGateway.cs
βββ Application/
β βββ Commands/
β β βββ PlaceOrderCommand.cs
β βββ Queries/
β β βββ GetOrderQuery.cs
β βββ Handlers/
β β βββ PlaceOrderHandler.cs
β β βββ GetOrderHandler.cs
β βββ DTOs/
β βββ OrderDto.cs
βββ Infrastructure/
β βββ Persistence/
β β βββ AppDbContext.cs
β β βββ EfOrderRepository.cs
β βββ ExternalServices/
β β βββ StripePaymentGateway.cs
β βββ DependencyInjection.cs
βββ Api/
βββ Controllers/
β βββ OrdersController.cs
βββ Program.cs
Key Benefits
- Testability - Domain logic isolated from infrastructure
- Flexibility - Easy to swap adapters (e.g., change database)
- Maintainability - Clear boundaries and responsibilities
- Domain Focus - Business logic at the center
Sources
C#/Teorie/DomainDrivenHexagon.png