Skip to main content
๐Ÿ—๏ธArchitecture Patterns

๐Ÿ”ท Hexagonal Architecture: The Complete Guide to Ports and Adapters

Hexagonal Architecture, also known as Ports and Adapters, is a software design pattern introduced by Alistair Cockburn in 2005. Its core idea is deceptivel...

โ€ข๐Ÿ“– 12 min read

๐Ÿ”ท Hexagonal Architecture: The Complete Guide to Ports and Adapters

Hexagonal Architecture, also known as Ports and Adapters, is a software design pattern introduced by Alistair Cockburn in 2005. Its core idea is deceptively simple: isolate your business logic from external concerns โ€” databases, APIs, UI frameworks, message queues โ€” by placing the domain at the center and connecting everything else through well-defined interfaces. The result is a system that is inherently testable, flexible, and resilient to change. If you have ever struggled with tightly coupled code where swapping a database or adding a new API endpoint required touching dozens of files, hexagonal architecture is the antidote.

Unlike traditional layered architecture where dependencies flow top-down, hexagonal architecture enforces a strict rule: all dependencies point inward toward the domain. Nothing in your core business logic knows about HTTP, SQL, or any specific framework. This single constraint unlocks extraordinary testability and adaptability.


๐Ÿงฉ Core Concepts: Domain, Ports, and Adapters

The hexagonal architecture model is built on three fundamental building blocks. Understanding each one is critical before you write a single line of code.

The Domain (Application Core)

The domain sits at the very center of the hexagon. It contains your business rules, entities, value objects, and domain services. The domain has zero dependencies on external libraries or frameworks. It speaks only in terms of your business language. For an e-commerce system, the domain knows about Order, Product, Customer, and PaymentResult โ€” it knows nothing about HttpRequest, SqlConnection, or JsonSerializer.

Ports (Interfaces)

Ports are the boundaries of your domain. They are interfaces (or abstract classes) that define how the outside world interacts with the domain and how the domain interacts with the outside world. There are two types:

  • Driving Ports (Primary/Inbound): These define what the application can do. They are use-case interfaces that external actors call into. Example: IOrderService with a method PlaceOrder().
  • Driven Ports (Secondary/Outbound): These define what the application needs from the outside world. They are interfaces the domain calls when it needs to persist data, send notifications, or call external services. Example: IOrderRepository with a method Save(Order order).

Adapters (Implementations)

Adapters are concrete implementations that plug into ports. They translate between external technologies and domain concepts:

  • Driving Adapters (Primary/Inbound): REST controllers, gRPC handlers, CLI commands, GraphQL resolvers โ€” anything that receives input and calls a driving port.
  • Driven Adapters (Secondary/Outbound): Database repositories, email services, third-party API clients, message queue publishers โ€” anything the domain delegates to via a driven port.

The key insight is that adapters depend on ports, never the reverse. Your domain defines the contract; adapters conform to it. This is the essence of the Dependency Inversion Principle.


๐Ÿ”„ Dependency Inversion: The Engine Behind the Hexagon

The Dependency Inversion Principle (DIP) states that high-level modules should not depend on low-level modules โ€” both should depend on abstractions. Hexagonal architecture is DIP applied at the architectural level.

In a traditional layered approach, your service layer calls a concrete SqlOrderRepository. If you want to swap to MongoDB, you rewrite the service. In hexagonal architecture, the service depends on IOrderRepository (a port). The SQL implementation is just one adapter. Swapping it requires zero changes to the domain.

This inversion is enforced through dependency injection. At application startup (the composition root), you wire adapters to ports:

// C# Composition Root
services.AddScoped<IOrderRepository, SqlOrderRepository>();
services.AddScoped<INotificationService, EmailNotificationService>();
services.AddScoped<IOrderService, OrderService>();

The domain never instantiates its own dependencies. It declares what it needs via ports, and the composition root satisfies those needs with adapters. This is how you achieve true decoupling.


๐Ÿงช How Hexagonal Architecture Supercharges Testability

Testability is arguably the single greatest benefit of hexagonal architecture. Because every external dependency is behind a port (interface), you can substitute test doubles โ€” mocks, stubs, fakes โ€” with zero friction.

Consider testing an order placement use case. In a tightly coupled system, your test would need a running database, an email server, and possibly a payment gateway. With hexagonal architecture:

// C# Unit Test โ€” Domain logic tested in complete isolation
[Test]
public void PlaceOrder_WithValidItems_CreatesOrderSuccessfully()
{
    // Arrange โ€” fake adapters, no real infrastructure
    var fakeRepo = new InMemoryOrderRepository();
    var fakeNotifier = new FakeNotificationService();
    var service = new OrderService(fakeRepo, fakeNotifier);

    var command = new PlaceOrderCommand("customer-1", new[] {
        new OrderItem("SKU-100", 2, 29.99m)
    });

    // Act
    var result = service.PlaceOrder(command);

    // Assert
    Assert.That(result.IsSuccess, Is.True);
    Assert.That(fakeRepo.GetById(result.OrderId), Is.Not.Null);
    Assert.That(fakeNotifier.SentNotifications, Has.Count.EqualTo(1));
}

This test runs in milliseconds with no external dependencies. You are testing pure business logic. When bugs surface, you know exactly where to look. This is the power of keeping your domain clean.

You can also write integration tests by swapping in real adapters for specific ports while keeping others faked. For example, test your SQL adapter against a real database while faking the notification service. This granular control over test boundaries is unique to port-and-adapter architectures.


๐Ÿ“ Complete Folder Structure

A well-organized hexagonal project makes the architecture visible in the folder layout. Here is a recommended structure for a C# or Java project:

src/
โ”œโ”€โ”€ Domain/                         # The core โ€” zero external dependencies
โ”‚   โ”œโ”€โ”€ Entities/
โ”‚   โ”‚   โ”œโ”€โ”€ Order.cs
โ”‚   โ”‚   โ”œโ”€โ”€ OrderItem.cs
โ”‚   โ”‚   โ””โ”€โ”€ Customer.cs
โ”‚   โ”œโ”€โ”€ ValueObjects/
โ”‚   โ”‚   โ”œโ”€โ”€ Money.cs
โ”‚   โ”‚   โ””โ”€โ”€ Address.cs
โ”‚   โ”œโ”€โ”€ Exceptions/
โ”‚   โ”‚   โ””โ”€โ”€ InsufficientStockException.cs
โ”‚   โ””โ”€โ”€ Events/
โ”‚       โ””โ”€โ”€ OrderPlacedEvent.cs
โ”‚
โ”œโ”€โ”€ Application/                    # Use cases โ€” orchestrates domain logic
โ”‚   โ”œโ”€โ”€ Ports/
โ”‚   โ”‚   โ”œโ”€โ”€ Inbound/
โ”‚   โ”‚   โ”‚   โ”œโ”€โ”€ IOrderService.cs
โ”‚   โ”‚   โ”‚   โ””โ”€โ”€ ICustomerService.cs
โ”‚   โ”‚   โ””โ”€โ”€ Outbound/
โ”‚   โ”‚       โ”œโ”€โ”€ IOrderRepository.cs
โ”‚   โ”‚       โ”œโ”€โ”€ IPaymentGateway.cs
โ”‚   โ”‚       โ””โ”€โ”€ INotificationService.cs
โ”‚   โ”œโ”€โ”€ Services/
โ”‚   โ”‚   โ”œโ”€โ”€ OrderService.cs
โ”‚   โ”‚   โ””โ”€โ”€ CustomerService.cs
โ”‚   โ””โ”€โ”€ DTOs/
โ”‚       โ”œโ”€โ”€ PlaceOrderCommand.cs
โ”‚       โ””โ”€โ”€ OrderResult.cs
โ”‚
โ”œโ”€โ”€ Adapters/
โ”‚   โ”œโ”€โ”€ Inbound/                    # Driving adapters
โ”‚   โ”‚   โ”œโ”€โ”€ Rest/
โ”‚   โ”‚   โ”‚   โ”œโ”€โ”€ OrderController.cs
โ”‚   โ”‚   โ”‚   โ””โ”€โ”€ CustomerController.cs
โ”‚   โ”‚   โ””โ”€โ”€ Grpc/
โ”‚   โ”‚       โ””โ”€โ”€ OrderGrpcService.cs
โ”‚   โ””โ”€โ”€ Outbound/                   # Driven adapters
โ”‚       โ”œโ”€โ”€ Persistence/
โ”‚       โ”‚   โ”œโ”€โ”€ SqlOrderRepository.cs
โ”‚       โ”‚   โ””โ”€โ”€ OrderDbContext.cs
โ”‚       โ”œโ”€โ”€ Payment/
โ”‚       โ”‚   โ””โ”€โ”€ StripePaymentGateway.cs
โ”‚       โ””โ”€โ”€ Notification/
โ”‚           โ””โ”€โ”€ EmailNotificationService.cs
โ”‚
โ””โ”€โ”€ Infrastructure/                 # Composition root and configuration
    โ”œโ”€โ”€ DependencyInjection.cs
    โ””โ”€โ”€ Program.cs

Notice the dependency direction: Adapters reference Application (for port interfaces), Application references Domain, and Domain references nothing external. The Infrastructure layer wires everything together at startup.


๐Ÿ’ป Real Code Structure Example (C#)

Let us walk through a complete implementation of the order placement use case.

Domain Entity

// Domain/Entities/Order.cs
public class Order
{
    public Guid Id { get; private set; }
    public string CustomerId { get; private set; }
    public List<OrderItem> Items { get; private set; }
    public decimal TotalAmount => Items.Sum(i => i.Quantity * i.UnitPrice);
    public OrderStatus Status { get; private set; }

    public Order(string customerId, List<OrderItem> items)
    {
        if (!items.Any())
            throw new ArgumentException("Order must have at least one item.");

        Id = Guid.NewGuid();
        CustomerId = customerId;
        Items = items;
        Status = OrderStatus.Pending;
    }

    public void MarkAsPaid()
    {
        if (Status != OrderStatus.Pending)
            throw new InvalidOperationException("Only pending orders can be paid.");
        Status = OrderStatus.Paid;
    }
}

Ports (Interfaces)

// Application/Ports/Inbound/IOrderService.cs
public interface IOrderService
{
    OrderResult PlaceOrder(PlaceOrderCommand command);
}

// Application/Ports/Outbound/IOrderRepository.cs
public interface IOrderRepository
{
    void Save(Order order);
    Order GetById(Guid id);
}

// Application/Ports/Outbound/IPaymentGateway.cs
public interface IPaymentGateway
{
    PaymentResult Charge(string customerId, decimal amount);
}

Application Service (Use Case Implementation)

// Application/Services/OrderService.cs
public class OrderService : IOrderService
{
    private readonly IOrderRepository _orderRepo;
    private readonly IPaymentGateway _paymentGateway;
    private readonly INotificationService _notifier;

    public OrderService(
        IOrderRepository orderRepo,
        IPaymentGateway paymentGateway,
        INotificationService notifier)
    {
        _orderRepo = orderRepo;
        _paymentGateway = paymentGateway;
        _notifier = notifier;
    }

    public OrderResult PlaceOrder(PlaceOrderCommand command)
    {
        var items = command.Items
            .Select(i => new OrderItem(i.Sku, i.Quantity, i.UnitPrice))
            .ToList();

        var order = new Order(command.CustomerId, items);

        var payment = _paymentGateway.Charge(
            command.CustomerId, order.TotalAmount);

        if (!payment.Success)
            return OrderResult.Failed("Payment declined.");

        order.MarkAsPaid();
        _orderRepo.Save(order);
        _notifier.SendOrderConfirmation(order);

        return OrderResult.Succeeded(order.Id);
    }
}

Driven Adapter (Outbound)

// Adapters/Outbound/Persistence/SqlOrderRepository.cs
public class SqlOrderRepository : IOrderRepository
{
    private readonly OrderDbContext _db;

    public SqlOrderRepository(OrderDbContext db) => _db = db;

    public void Save(Order order)
    {
        _db.Orders.Add(order);
        _db.SaveChanges();
    }

    public Order GetById(Guid id) =>
        _db.Orders.Include(o => o.Items).FirstOrDefault(o => o.Id == id);
}

Driving Adapter (Inbound)

// Adapters/Inbound/Rest/OrderController.cs
[ApiController]
[Route("api/orders")]
public class OrderController : ControllerBase
{
    private readonly IOrderService _orderService;

    public OrderController(IOrderService orderService) =>
        _orderService = orderService;

    [HttpPost]
    public IActionResult PlaceOrder([FromBody] PlaceOrderRequest request)
    {
        var command = new PlaceOrderCommand(
            request.CustomerId, request.Items);

        var result = _orderService.PlaceOrder(command);

        return result.IsSuccess
            ? Ok(new { result.OrderId })
            : BadRequest(new { result.Error });
    }
}

๐Ÿ“Š Hexagonal vs. Layered Architecture

Aspect Layered Architecture Hexagonal Architecture
Dependency Direction Top-down (UI โ†’ Service โ†’ Data) Outside-in (all point toward domain)
Domain Isolation Domain often leaks into data layer Domain is fully isolated at center
Testability Requires mocking concrete classes All dependencies are interfaces (ports)
Swapping Infrastructure Requires changes across layers Swap adapter; domain untouched
Multiple Entry Points Typically one (e.g., HTTP) Natural support (REST, gRPC, CLI, etc.)
Complexity Simpler, fewer abstractions More interfaces, steeper learning curve
Best For Simple CRUD, small applications Complex domains, long-lived systems

โœ… Pros and Cons

Advantages

  • Technology Independence: Your domain logic survives framework migrations. Swap REST for GraphQL, PostgreSQL for DynamoDB โ€” the core never changes.
  • Superior Testability: Unit test your entire business logic with in-memory fakes in milliseconds.
  • Parallel Development: Teams can work on adapters independently. The API team builds controllers while the persistence team builds repositories โ€” both code against the same port interfaces.
  • Multiple Delivery Mechanisms: Expose the same use case via REST, gRPC, CLI, and event handlers without duplicating business logic.
  • Explicit Boundaries: The architecture makes dependencies visible. New developers can quickly understand what depends on what.

Disadvantages

  • Increased Indirection: More interfaces and classes mean more files to navigate. For simple CRUD, this is overkill.
  • Steeper Learning Curve: Developers unfamiliar with SOLID principles and dependency injection may struggle initially.
  • Boilerplate: Small features require creating a port interface, an adapter, a use-case service, and wiring โ€” even if the implementation is trivial.
  • Over-engineering Risk: Applying this pattern to a simple microservice with one database and one API endpoint adds complexity with little benefit.

๐ŸŽฏ When to Use Hexagonal Architecture

Hexagonal architecture shines in specific contexts. Use it when:

  • Your domain has complex business rules that must be tested independently from infrastructure.
  • You anticipate swapping infrastructure โ€” migrating databases, changing cloud providers, or supporting multiple delivery channels.
  • You are building a long-lived system where maintainability matters more than speed of initial delivery.
  • Your team practices Domain-Driven Design (DDD) โ€” hexagonal architecture is the natural architectural companion to DDD.
  • You need multiple entry points โ€” a web API, a CLI tool, an event consumer, and a scheduled job all sharing the same business logic.

Avoid it for simple CRUD applications, prototypes, or systems where the domain logic is thin and unlikely to evolve significantly.


๐ŸŒ Real-World Adoption

Hexagonal architecture is not an academic exercise โ€” it is battle-tested at scale:

  • Netflix: Uses hexagonal principles extensively in their microservices to isolate domain logic from infrastructure concerns, enabling rapid technology swaps.
  • Spotify: Backend services follow ports-and-adapters patterns to maintain clean separation between business logic and delivery mechanisms.
  • Government Digital Services (UK GDS): Adopted hexagonal architecture to build long-lived, maintainable public services where technology decisions must remain reversible.
  • Zalando: Their engineering guidelines recommend hexagonal architecture for services with complex business domains, particularly in their checkout and payment systems.

The pattern is also a cornerstone of Clean Architecture (Robert C. Martin) and Onion Architecture (Jeffrey Palermo), both of which extend the same core principle of dependency inversion at the architectural level.


โ“ Frequently Asked Questions

What is the difference between hexagonal architecture and clean architecture?

They share the same fundamental principle โ€” dependencies point inward toward the domain. Hexagonal architecture focuses on the metaphor of ports (interfaces) and adapters (implementations) with the domain at the center. Clean architecture adds more explicit layers (Entities, Use Cases, Interface Adapters, Frameworks) and prescribes specific rules about what each layer can reference. In practice, a well-implemented hexagonal architecture and clean architecture look nearly identical in code. Think of clean architecture as a more prescriptive formalization of the same ideas.

Can I use hexagonal architecture with microservices?

Absolutely โ€” they complement each other well. Each microservice can be structured internally using hexagonal architecture. The service boundary defines the outer hexagon, ports define how the service communicates (REST, events, gRPC), and the domain core contains the bounded context logic. This combination gives you both inter-service decoupling (microservices) and intra-service decoupling (hexagonal). However, for very simple microservices that only proxy data, the overhead may not be justified.

How does hexagonal architecture handle cross-cutting concerns like logging and authentication?

Cross-cutting concerns live in the adapter or infrastructure layer, never in the domain. Logging can be implemented as a decorator around port interfaces โ€” for example, a LoggingOrderRepository that wraps IOrderRepository, logs each call, and delegates to the real implementation. Authentication belongs in driving adapters (e.g., middleware in your REST controller layer). The domain should never know about HTTP headers or JWT tokens. Use the decorator pattern and middleware pipelines to keep these concerns out of your business logic.

Is hexagonal architecture the same as ports and adapters?

Yes. Hexagonal Architecture and Ports and Adapters are two names for the same pattern, both coined by Alistair Cockburn. The hexagonal name comes from the visual diagram where the application core is drawn as a hexagon with ports on each face. The six sides have no special meaning โ€” the hexagon shape was chosen simply to allow room to draw multiple ports and adapters around the boundary, unlike a circle or rectangle which would be visually cluttered.

How do I migrate an existing layered application to hexagonal architecture?

Migrate incrementally rather than rewriting. Start by extracting interfaces for your repository and external service classes โ€” these become your outbound ports. Next, move your business logic into domain entities and application services that depend only on these interfaces. Then restructure your controllers to call application services through inbound port interfaces. Finally, reorganize folders to reflect the hexagonal structure. Use the Strangler Fig pattern to migrate one use case at a time, keeping the system functional throughout the transition.


Related Articles