| name | hexagonal |
| description | Hexagonal Architecture (Ports and Adapters), Onion Architecture, and their relationship to Clean Architecture -- enabling technology-independent domain logic with high testability.
USE FOR: hexagonal architecture, ports and adapters, onion architecture, driving/driven adapters, technology-independent domain design, adapter-based testability
DO NOT USE FOR: clean architecture layers specifically (use dev/craftsmanship/clean-architecture), microservice boundaries (use microservices), domain model design (use domain-driven-design)
|
| license | MIT |
| metadata | {"displayName":"Hexagonal Architecture","author":"Tyler-R-Kendrick"} |
| compatibility | claude, copilot, cursor |
| references | [{"title":"Alistair Cockburn — Hexagonal Architecture (Ports and Adapters)","url":"https://alistair.cockburn.us/hexagonal-architecture/"},{"title":"Hexagonal Architecture — Wikipedia","url":"https://en.wikipedia.org/wiki/Hexagonal_architecture_(software)"}] |
Hexagonal Architecture (Ports and Adapters)
Overview
Hexagonal Architecture, introduced by Alistair Cockburn in 2005, organizes an application so that the core domain logic is isolated from external concerns (databases, APIs, UIs, message brokers) through ports (interfaces) and adapters (implementations). The goal is to make the application equally drivable by users, programs, automated tests, or batch scripts -- and equally connected to any external system.
The architecture is also known as Ports and Adapters. It shares the same fundamental principle as Onion Architecture (Jeffrey Palermo, 2008) and Clean Architecture (Robert C. Martin): dependencies point inward; the domain depends on nothing external.
The Hexagonal Diagram
Driving Side (Primary)
(things that USE the app)
REST CLI Tests Events
│ │ │ │
▼ ▼ ▼ ▼
┌─────────────────────────────────┐
│ Primary Adapters │
│ (implement driving ports) │
│ │
┌─────┤─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─┤─────┐
│ │ Primary Ports │ │
│ │ (interfaces the app exposes) │ │
│ │ │ │
│ │ ┌───────────────────┐ │ │
│ │ │ │ │ │
│ │ │ Domain Model │ │ │
│ │ │ (Pure Business │ │ │
│ │ │ Logic) │ │ │
│ │ │ │ │ │
│ │ └───────────────────┘ │ │
│ │ │ │
│ │ Secondary Ports │ │
│ │ (interfaces the app needs) │ │
└─────┤─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─┤─────┘
│ Secondary Adapters │
│ (implement driven ports) │
└─────────────────────────────────┘
│ │ │ │
▼ ▼ ▼ ▼
Postgres Redis Stripe Kafka
Driven Side (Secondary)
(things the app USES)
Core Concepts
Ports (Interfaces)
Ports define the boundaries of the application. They are interfaces -- contracts that describe what the application can do (primary/driving) or what the application needs (secondary/driven).
| Port Type | Also Called | Direction | Purpose | Example |
|---|
| Primary Port | Driving Port | Inbound | Defines what the app offers to the outside world | IOrderService.PlaceOrder(...) |
| Secondary Port | Driven Port | Outbound | Defines what the app requires from the outside world | IOrderRepository.Save(...), IPaymentGateway.Charge(...) |
// Primary port — what the application offers
public interface IOrderService
{
Task<OrderId> PlaceOrder(PlaceOrderCommand command);
Task<OrderDto> GetOrder(OrderId id);
Task CancelOrder(OrderId id);
}
// Secondary port — what the application needs
public interface IOrderRepository
{
Task<Order?> FindById(OrderId id);
Task Save(Order order);
}
// Secondary port — what the application needs
public interface IPaymentGateway
{
Task<PaymentResult> Charge(Money amount, PaymentMethod method);
Task Refund(PaymentId paymentId, Money amount);
}
Adapters (Implementations)
Adapters are concrete implementations that connect ports to specific technologies. They live outside the domain core.
| Adapter Type | Also Called | Implements | Example |
|---|
| Primary Adapter | Driving Adapter | Uses primary ports | REST controller, gRPC handler, CLI command, test harness |
| Secondary Adapter | Driven Adapter | Implements secondary ports | PostgreSQL repository, Stripe payment adapter, Kafka publisher |
// Primary adapter — REST controller drives the application
[ApiController]
public class OrdersController : ControllerBase
{
private readonly IOrderService _orderService; // Primary port
[HttpPost]
public async Task<IActionResult> PlaceOrder(PlaceOrderRequest request)
{
var command = MapToCommand(request);
var orderId = await _orderService.PlaceOrder(command);
return Created($"/orders/{orderId}", new { orderId });
}
}
// Secondary adapter — PostgreSQL implements the repository port
public class PostgresOrderRepository : IOrderRepository
{
private readonly DbContext _db;
public async Task<Order?> FindById(OrderId id)
{
return await _db.Orders
.Include(o => o.Lines)
.FirstOrDefaultAsync(o => o.Id == id);
}
public async Task Save(Order order) { ... }
}
// Secondary adapter — Stripe implements the payment port
public class StripePaymentGateway : IPaymentGateway
{
private readonly StripeClient _stripe;
public async Task<PaymentResult> Charge(Money amount, PaymentMethod method)
{
var intent = await _stripe.PaymentIntents.CreateAsync(...);
return MapToResult(intent);
}
}
Driving vs. Driven Side
| Aspect | Driving (Primary) Side | Driven (Secondary) Side |
|---|
| Who initiates | External actor drives the application | Application drives external systems |
| Port direction | Inbound (app receives calls) | Outbound (app makes calls) |
| Adapter role | Translates external input into domain calls | Translates domain calls into external system interactions |
| Dependency direction | Adapter depends on port (calls it) | Adapter implements port (the domain defines the interface) |
| Examples | HTTP controller, CLI, test, event consumer | Database, API client, message publisher, file system |
Code Structure Example
src/
OrderService/
Domain/ # Pure domain model (no dependencies)
Order.cs
OrderLine.cs
Money.cs
OrderStatus.cs
Ports/
Primary/ # What the app offers
IOrderService.cs
Commands/
PlaceOrderCommand.cs
CancelOrderCommand.cs
Queries/
GetOrderQuery.cs
Secondary/ # What the app needs
IOrderRepository.cs
IPaymentGateway.cs
IInventoryClient.cs
IEventPublisher.cs
Application/ # Use case orchestration
OrderApplicationService.cs # Implements IOrderService
Adapters/
Primary/ # Driving adapters
Rest/
OrdersController.cs
Grpc/
OrderGrpcService.cs
Cli/
OrderCliCommand.cs
Secondary/ # Driven adapters
Persistence/
PostgresOrderRepository.cs
Payment/
StripePaymentGateway.cs
Messaging/
KafkaEventPublisher.cs
Composition/ # Wires everything together (DI)
ServiceRegistration.cs
The Dependency Rule
The fundamental rule shared by Hexagonal, Onion, and Clean Architecture:
Dependencies point inward. Inner layers know nothing about outer layers.
┌─────────────────────────────────────────┐
│ Adapters (outermost) │
│ ┌─────────────────────────────────┐ │
│ │ Ports / Application │ │
│ │ ┌─────────────────────────┐ │ │
│ │ │ Domain Model │ │ │
│ │ │ (innermost, no deps) │ │ │
│ │ └─────────────────────────┘ │ │
│ └─────────────────────────────────┘ │
└─────────────────────────────────────────┘
Outer depends on inner. Never the reverse.
- The domain has zero external dependencies. No framework imports, no database references, no HTTP concepts.
- Ports are defined by the domain/application layer using domain language.
- Adapters depend on ports (and on external libraries), never the reverse.
- Composition root (startup/DI configuration) wires adapters to ports.
Comparison: Hexagonal vs. Onion vs. Clean Architecture
| Aspect | Hexagonal (Cockburn) | Onion (Palermo) | Clean (Martin) |
|---|
| Core idea | Ports and Adapters | Concentric layers | Dependency Rule |
| Visualization | Hexagon with ports | Concentric circles | Concentric circles |
| Inner layer | Domain Model | Domain Model | Entities |
| Boundary definition | Ports (interfaces) | Layer interfaces | Use Case boundaries |
| Outer layer | Adapters | Infrastructure | Frameworks & Drivers |
| Key emphasis | Symmetry between driving/driven | Layer discipline | Use cases as central organizing concept |
| Dependency direction | Inward | Inward | Inward |
They are the same fundamental idea expressed differently. All three:
- Isolate the domain from infrastructure.
- Use interfaces (ports) at boundaries.
- Enforce the dependency rule: inner layers never reference outer layers.
- Enable technology swaps without changing business logic.
The practical differences are in emphasis and vocabulary, not in principle.
Onion Architecture Layers (Palermo)
┌─────────────────────────────────────────┐
│ Infrastructure & UI (outermost) │
│ ┌─────────────────────────────────┐ │
│ │ Application Services │ │
│ │ ┌─────────────────────────┐ │ │
│ │ │ Domain Services │ │ │
│ │ │ ┌─────────────────┐ │ │ │
│ │ │ │ Domain Model │ │ │ │
│ │ │ │ (Entities, │ │ │ │
│ │ │ │ Value Objects)│ │ │ │
│ │ │ └─────────────────┘ │ │ │
│ │ └─────────────────────────┘ │ │
│ └─────────────────────────────────┘ │
└─────────────────────────────────────────┘
Testability: The Primary Benefit
The greatest practical benefit of hexagonal architecture is testability. Because the domain depends only on ports (interfaces), you can test business logic without any infrastructure.
// Test the domain using mock adapters — no database, no HTTP, no Stripe
[Test]
public async Task PlaceOrder_WithValidItems_ConfirmsOrder()
{
// Arrange — mock secondary ports
var orderRepo = new InMemoryOrderRepository();
var paymentGateway = new FakePaymentGateway(alwaysSucceeds: true);
var eventPublisher = new SpyEventPublisher();
// The application service uses ports, not concrete adapters
var service = new OrderApplicationService(
orderRepo, paymentGateway, eventPublisher);
// Act — drive through primary port
var orderId = await service.PlaceOrder(new PlaceOrderCommand
{
CustomerId = "cust-1",
Items = new[] { new OrderItem("prod-1", 2, 25.00m) }
});
// Assert — verify domain behavior
var order = await orderRepo.FindById(orderId);
Assert.Equal(OrderStatus.Confirmed, order.Status);
Assert.Single(eventPublisher.PublishedEvents
.OfType<OrderConfirmed>());
}
Testing Strategy by Layer
| Layer | Test Type | What to Test | Infrastructure Needed |
|---|
| Domain | Unit tests | Business rules, invariants, calculations | None (pure logic) |
| Application | Unit tests with mocks | Use case orchestration, event publishing | Mock adapters |
| Primary Adapters | Integration tests | Request mapping, serialization, routing | HTTP test server |
| Secondary Adapters | Integration tests | Database queries, API calls, serialization | Real or containerized infrastructure |
| Composition | Smoke / E2E tests | Full system wiring, happy path | Full infrastructure |
Common Mistakes
| Mistake | Problem | Fix |
|---|
| Domain imports framework | Domain coupled to infrastructure; hard to test | Remove all framework dependencies from domain layer |
| Adapter logic in domain | Business logic leaks into controllers or repositories | Move logic to domain model or application service |
| Port too broad | Interface with 20 methods; hard to mock, violates ISP | Split into focused interfaces (Interface Segregation Principle) |
| Skipping ports for "simplicity" | Application calls database directly; loses swappability and testability | Always define a port even if you only have one adapter |
| Anemic domain + fat service | Domain model is just data; all logic in application service | Enrich the domain model with behavior (see dev/architecture/domain-driven-design) |
Best Practices
- Keep the domain model completely free of infrastructure dependencies. No ORM attributes, no HTTP concepts, no serialization annotations in the domain layer.
- Define ports using domain language, not technology language.
IOrderRepository.Save(Order), not IDatabaseContext.ExecuteCommand(SQL).
- Use dependency injection to wire adapters to ports at the composition root.
- Write the majority of tests against ports (mock adapters), not against infrastructure. This gives you fast, reliable tests.
- Start with one adapter per port. Add additional adapters when you actually need them (e.g., switching databases, adding a CLI interface).
- Use the hexagonal structure to enable incremental migration: swap one adapter at a time without touching the domain.
- Combine with DDD (see
dev/architecture/domain-driven-design) for rich domain modeling inside the hexagon.
- The hexagonal shape is a metaphor for symmetry -- there is no inherent "top" or "bottom." Any adapter on any side is equally first-class.