Hexagonal Architecture — A Deep Dive
Hexagonal Architecture (also known as the Ports and Adapters pattern) is a software architecture style intended to keep application core logic independent from external concerns (UI, databases, external services, messaging systems). It aims to make applications easier to test, evolve, and integrate by decoupling the business rules from their delivery mechanisms and infrastructure.
This article covers history, key concepts, theoretical foundations, practical implementations, comparisons with related architectures, code examples, testing strategies, migration guidance, common pitfalls, and future implications.
Table of contents
- Introduction and motivations
- History and origin
- Core concepts
- Ports and Adapters
- Primary (driving) vs Secondary (driven) actors
- Dependency rule and direction of flow
- Hexagon metaphor
- Theoretical foundations and principles
- Single Responsibility Principle
- Dependency Inversion Principle
- Separation of concerns
- Testability and inversion of control
- Typical architecture and flows
- Implementing Hexagonal Architecture
- Design steps and boundaries
- Folder/project structure examples
- Example: Java + Spring (simple use case)
- Example: Node/TypeScript (simple use case)
- Example: C#/.NET (simple use case)
- Complementary patterns and techniques
- Domain-Driven Design (DDD)
- CQRS and Event Sourcing
- Messaging and event-driven systems
- Testing strategies
- Unit testing
- Integration testing with adapters
- Contract testing
- End-to-end testing considerations
- Deployment and operational considerations
- Microservices and Hexagonal Architecture
- Observability and monitoring
- Comparisons with other architectures
- Layered (n-tier)
- Onion Architecture
- Clean Architecture
- Migration strategies and refactoring approaches
- Common pitfalls and anti-patterns
- Case studies and examples
- Future trends and implications
- Practical checklist and recommendations
- Further reading
Introduction and motivations
When building software, you inevitably deal with two kinds of code:
- Application (business) logic — the core domain rules and policies.
- Infrastructure and delivery concerns — databases, web frameworks, messaging, UI, third-party APIs.
Hexagonal Architecture enforces a clear boundary between these concerns so the core domain remains independent from frameworks and external systems. This yields benefits:
- Easier unit testing of domain logic without infrastructure dependencies.
- Replaceable infrastructure (swap databases, UI, queue) with minimal impact.
- Clear communication boundaries and better modularity.
- Encourages explicit modeling of interaction points (ports) and their implementations (adapters).
History and origin
Hexagonal Architecture was proposed by Alistair Cockburn around 2005–2006. Cockburn's intent was to eliminate coupling between business logic and technology frameworks or delivery mechanisms and create a clear, testable architecture that supports different kinds of interactions with the application.
Cockburn used the hexagon as a metaphor: the application's core sits in the center, and each face of the hexagon is a port (an interface for communication). Adapters plug into ports to provide implementations for the outside world. Over time the pattern became widely known as "Ports and Adapters" and influenced other architectural styles: Onion Architecture (Jeffrey Palermo), Clean Architecture (Robert C. Martin), and more.
Core concepts
Ports and Adapters
- Port: An abstract interface representing a required service or an offered capability. Ports are defined by the application core. They describe how the application expects to interact with external actors or services. Examples:
- Repositories (persisting and retrieving domain entities)
- Notification service (send emails)
- Input interfaces (HTTP controllers, CLI commands)
- Adapter: Concrete implementation that translates between the external world and the port. Adapters implement ports and live in the infrastructure layer. Examples:
- SQL repository adapter using JDBC/ORM
- REST controller adapter translating HTTP into application commands
- Message queue adapter that routes messages to domain services
The key is that adapters depend on ports (interfaces), not the other way around.
Primary (driving) vs Secondary (driven) actors
- Primary (driving) actors initiate interaction with the application: users, cron jobs, external systems calling the app. They are implemented by "driving adapters" that call the application through ports.
- Secondary (driven) actors are services/devices the application calls: DB, external APIs, message brokers, email providers. They are implemented by "driven adapters" that implement ports the application expects.
This separation clarifies who initiates interactions and which parts are replaceable.
Dependency direction
The dependency rule: domain/core code should not depend on frameworks or infrastructure. Dependencies point inwards: adapters depend on the core interfaces. This aligns with the Dependency Inversion Principle (DIP).
Hexagon metaphor
Cockburn used a hexagon to show the app core surrounded by ports. The number of sides is arbitrary — the hexagon is a visual aid. The core defines the protocol, adapters plug into the ports, and the outside world interacts via adapters.
ASCII diagram:
`` +----------+ / \ UI DB | Core | CLI ->\ /-> Messaging +----------+ ``
Theoretical foundations and principles
Hexagonal Architecture embodies several software design principles:
- Single Responsibility Principle: Keep domain logic focused on business rules; infrastructure concerns are separated into adapters.
- Dependency Inversion Principle: High-level modules (domain) should not depend on low-level modules (DB). Both should depend on abstractions (ports).
- Separation of Concerns: Each layer handles a distinct problem domain.
- Interface Segregation: Ports should be focused interfaces tailored to use-cases.
- Inversion of Control: The control flow is inverted — the application defines what it needs; adapters are injected.
These principles improve modularity, testability, and maintainability.
Typical architecture and flows
High-level components:
- Domain model / business logic: Entities, value objects, domain services (pure, no I/O).
- Application services / use cases: Orchestrate domain operations (still free of infrastructure).
- Ports: Interfaces exposed by the application to be implemented by adapters.
- Adapters:
- Driving adapters (controllers, jobs, CLI) invoke application use cases.
- Driven adapters (persistence, remote APIs, email clients) implement ports used by use cases.
Flow examples:
- User sends an HTTP request (driving adapter: Controller) → Controller calls application use case via port → Use case executes domain logic, uses repository port → Repository port implemented by DB adapter → DB adapter does I/O → Data returned to use case → Use case returns DTO to controller → Controller maps to HTTP response.
- Application needs to publish events → Application calls a MessagePort interface → Messaging adapter implements MessagePort sending messages to Kafka/RabbitMQ.
Implementing Hexagonal Architecture
Design steps and boundaries
- Identify core use cases: what the application must do.
- Define application services / use case classes that implement these use cases and express dependencies via interfaces (ports).
- Design domain model and domain services (pure behavior).
- Define ports for external interactions (persistence, external APIs, notifications).
- Implement adapters for each port (DB repositories, REST clients).
- Implement driving adapters: controllers, CLI, scheduled tasks.
- Wire dependencies via dependency injection at the composition root (application startup).
Key rule: Code inside the core (domain and use cases) must depend only on ports (interfaces) and domain objects.
Folder/project structure examples
Monolith single repo:
- src/
- domain/
- model/
- services/
- application/
- usecases/
- ports/
- adapters/
- inbound/ (http, cli)
- outbound/ (db, http clients, email)
- config/ (composition root, DI)
- tests/
Microservice with multiple projects (example in JVM):
- service-core (domain + application ports + DTOs)
- service-adapters (inbound + outbound adapters)
- service-runner (composition root + deployment artifacts)
Example: Java + Spring Boot (simplified)
Domain and port:
```java // domain/Order.java public class Order { private UUID id; private List items; // domain behavior... }
// ports/OrderRepository.java public interface OrderRepository { Optional findById(UUID id); void save(Order order); } ```
Use case (application service):
```java // application/PlaceOrderService.java @Component public class PlaceOrderService { private final OrderRepository orderRepository; private final PaymentGateway paymentGateway; // port for external payment service
public PlaceOrderService(OrderRepository orderRepository, PaymentGateway paymentGateway) { this.orderRepository = orderRepository; this.paymentGateway = paymentGateway; }
public OrderResult placeOrder(PlaceOrderCommand command) { // business logic, validation, domain entity creation // call paymentGateway.charge(...) // save via orderRepository.save(order) } } ```
Adapter (driven) — DB repository:
```java // adapters/outbound/JpaOrderRepository.java @Repository public class JpaOrderRepository implements OrderRepository { private final JpaOrderEntityRepository jpaRepo; // Spring Data JPA
public Optional findById(UUID id) { return jpaRepo.findById(id).map(this::toDomain); }
public void save(Order order) { jpaRepo.save(toEntity(order)); } } ```
Adapter (driving) — REST controller:
```java // adapters/inbound/OrderController.java @RestController @RequestMapping("/orders") public class OrderController { private final PlaceOrderService placeOrderService;
@PostMapping public ResponseEntity placeOrder(@RequestBody PlaceOrderRequest req) { var result = placeOrderService.placeOrder(req.toCommand()); return ResponseEntity.ok(OrderResponse.from(result)); } } ```
Composition root: Spring Boot config wires concrete adapters into constructors of the service (DI).
Example: Node / TypeScript
Interfaces (ports) and domain:
```ts // src/ports/orderRepository.ts import { Order } from "../domain/order";
export interface OrderRepository { findById(id: string): Promise ; save(order: Order): Promise ...