πŸ›οΈ

Domain-Driven Hexagonal Architecture

System Architecture Intermediate 1 min read 100 words
Clean Architecture Domain-Driven Design

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

  1. Testability - Domain logic isolated from infrastructure
  2. Flexibility - Easy to swap adapters (e.g., change database)
  3. Maintainability - Clear boundaries and responsibilities
  4. Domain Focus - Business logic at the center

Sources

  • C#/Teorie/DomainDrivenHexagon.png

πŸ“š Related Articles