Hexagonal Architecture (Ports and Adapters)
Hexagonal Architecture (Ports and Adapters)
"Allow an application to equally be driven by users, programs, automated test or batch scripts, and to be developed and tested in isolation from its eventual run-time devices and databases." — Alistair Cockburn, 2005
Intent
Hexagonal Architecture, also called Ports and Adapters, is Alistair Cockburn's 2005 formulation of the dependency inversion principle applied at the application level. Cockburn's original insight is symmetry: there is no "top" and "bottom" to the architecture. UI and database are both "outside" — neither is privileged. The left-side (driving) ports and right-side (driven) ports are the same mechanism applied in two directions.
The architecture separates the application into two zones: the application core (the hexagon) and the outside world (everything that touches it). The core contains all business logic. All outside actors — HTTP controllers, CLI tools, test harnesses, databases, message brokers — interact with the core exclusively through ports (interfaces) and adapters (technology-specific implementations of those ports).
Clean Architecture, Hexagonal Architecture, and Onion Architecture express the same foundational principle — dependency inversion toward the domain — with different vocabularies. Three separate notes exist because the vocabulary differences matter in practice: teams encounter these patterns by name in codebases and must work in the specific terminology already in use.
When NOT to Use
- CRUD-only applications where the overhead of port/adapter pairs exceeds testability benefits
- Simple scripts and utilities
- When the team has no plans to swap infrastructure implementations and testability of the core in isolation is not a requirement
When to Use
- When the application must be driven identically by HTTP, CLI, test harnesses, and batch jobs without code changes
- When infrastructure implementations need to be swapped — in-memory persistence for tests, PostgreSQL in production — without touching the application core
- When testability of the application core without any infrastructure dependency is a hard requirement
How It Works
The official terminology, from Cockburn's 2005 paper and 2024 book:
| Term | Definition |
|---|---|
| Port | An interface defined by the application — the named purpose of a conversation with the outside world |
| Adapter | Technology-specific translation code connecting an external actor to a port |
| Primary (Driving) port | Inbound interface — the application's API that external actors call (e.g., a use case interface) |
| Secondary (Driven) port | Outbound interface — what the application needs from infrastructure (e.g., a repository interface) |
| Driving adapter | Initiates interaction with the application (HTTP controller, CLI, test harness) — connects to a primary port |
| Driven adapter | The application drives it (database, email service, message broker) — implements a secondary port |
Critical rule — port interface location: The port interface (e.g., UserRepository) MUST be defined INSIDE the application core, NOT in the infrastructure layer. The infrastructure adapter (PostgresUserRepository) lives outside and implements it. This is the inversion that makes the architecture work. Any import from infrastructure/ or adapters/ inside application/ code is a violation.
[HTTP Controller] [Test Harness]
| |
(Driving Adapter) (Driving Adapter)
| |
v v
+----[Primary Port: OrderService interface]----+
| |
| APPLICATION CORE |
| (no framework, DB, or UI imports) |
| |
+----[Secondary Port: OrderRepository interface]-+
| |
(Driven Adapter) (Driven Adapter)
| |
[JPA Repository] [In-Memory Mock]
All three architectures express the same foundational principle — dependency inversion toward the domain — with different vocabularies.
| Dimension | Hexagonal (Cockburn, 2005) | Onion (Palermo, 2008) | Clean (Martin, 2012) |
|---|---|---|---|
| Layer Names | Port / Adapter / Application Core | Domain Model / Domain Services / Application Services / Infrastructure | Entities / Use Cases / Interface Adapters / Frameworks & Drivers |
| Dependency Rule Statement | "Outside actors connect via ports; the application core has no outward dependencies" | "All coupling toward the centre; outer layers depend on inner layers, never the reverse" | "Source code dependencies can only point inward. Nothing in an inner circle can know anything about something in an outer circle." |
| What It Prescribes Inside the Core | Nothing — Hexagonal does not specify the internal structure of the application core | Explicit separation of Domain Services from Application Services | Explicit separation of Entities (enterprise-wide rules) from Use Cases (application-specific rules) |
Architecture Diagram
Hexagonal-Architecture-diagram.excalidraw
TypeScript Example
Folder layout:
src/
application/ # The Hexagon (application core)
ports/
in/
OrderService.ts # Primary port (interface)
out/
OrderRepository.ts # Secondary port (interface) — DEFINED HERE
OrderServiceImpl.ts
adapters/
in/
http/
OrderController.ts # Driving adapter
out/
persistence/
PostgresOrderRepository.ts # Driven adapter — implements OrderRepository
infrastructure/
config/
Port direction pattern:
// Hexagonal Architecture — TypeScript
// src/application/ports/out/OrderRepository.ts
// SECONDARY PORT — defined INSIDE the hexagon
export interface OrderRepository {
save(order: Order): Promise<void>;
findById(id: string): Promise<Order | null>;
}
// src/adapters/out/persistence/PostgresOrderRepository.ts
// DRIVEN ADAPTER — lives OUTSIDE the hexagon, imports from core
import { OrderRepository } from '../../../application/ports/out/OrderRepository';
export class PostgresOrderRepository implements OrderRepository {
async save(order: Order): Promise<void> {
// PostgreSQL-specific implementation
}
async findById(id: string): Promise<Order | null> {
// PostgreSQL-specific query
return null;
}
}Java Example
Package layout:
com.app/
application/
ports/
in/
OrderService.java # Primary port
out/
OrderRepository.java # Secondary port — DEFINED HERE (not in infrastructure!)
OrderServiceImpl.java
adapters/
in/
web/
OrderController.java
out/
persistence/
JpaOrderRepository.java # Implements application.ports.out.OrderRepository
infrastructure/
config/
Port direction pattern:
// Hexagonal Architecture — Java
// application/ports/out/OrderRepository.java — INSIDE the hexagon
public interface OrderRepository {
void save(Order order);
Optional<Order> findById(OrderId id);
}
// adapters/out/persistence/JpaOrderRepository.java — OUTSIDE, implements port
@Repository
public class JpaOrderRepository implements OrderRepository {
// JPA-specific implementation
@Override
public void save(Order order) { /* ... */ }
@Override
public Optional<Order> findById(OrderId id) { /* ... */ }
}Enforcement Tooling
Install:
# TypeScript — dependency-cruiser
npm install --save-dev dependency-cruiser
# Java — ArchUnit (Maven)
# com.tngtech.archunit:archunit-junit5:1.3.0TypeScript — dependency-cruiser port-direction rule:
// .dependency-cruiser.js
{
forbidden: [{
name: "no-core-to-adapters",
comment: "Application core must not depend on adapters",
from: { path: "^src/application" },
to: { path: "^src/adapters" }
}, {
name: "no-core-to-infrastructure",
from: { path: "^src/application" },
to: { path: "^src/infrastructure" }
}]
}Java — ArchUnit:
@AnalyzeClasses(packages = "com.app")
class HexagonalArchitectureTest {
@ArchTest
static final ArchRule applicationCoreMustBeIsolated =
noClasses().that().resideInAPackage("..application..")
.should().dependOnClassesThat()
.resideInAnyPackage("..adapters..", "..infrastructure..");
}The secondary port interface (e.g., OrderRepository) MUST be defined in application/ports/out/, NOT in infrastructure/ or adapters/. Placing the interface in the infrastructure layer reverses the dependency inversion — the application core would then depend on infrastructure.
Warning sign: Any import from infrastructure/ or adapters/ inside application/ code. ts-arch is an alternative to dependency-cruiser for TS enforcement.
Related Concepts
- Clean-Architecture — Robert C. Martin's 2012 formulation; same dependency rule, different vocabulary (Entities / Use Cases / Interface Adapters / Frameworks & Drivers)
- Onion-Architecture — Jeffrey Palermo's 2008 formulation; extends Hexagonal by adding explicit Domain Services vs Application Services distinction inside the core
- Adapter-Pattern — the GoF structural pattern that Hexagonal adapters implement; every driven adapter is a GoF Adapter between a port interface and an external system
- Repository — the canonical secondary port example; the repository interface lives inside the application core, the implementation outside
- Bounded-Context — a Hexagonal hexagon naturally maps to a bounded context boundary; each context gets its own set of ports
- Anti-Corruption-Layer-Pattern — ACL sits at the secondary adapter boundary; driven adapters are the natural location for anti-corruption logic when integrating external systems
- CQRS-Pattern — command and query handlers are primary ports; CQRS partitions the driving side of the hexagon into separate command and query entry points
Sources
- Alistair Cockburn, "Hexagonal Architecture" (2005) — https://alistair.cockburn.us/hexagonal-architecture/
- Cockburn and Garrido de Paz, Hexagonal Architecture Explained (2024)
- Herberto Graca, "Ports and Adapters Architecture" (2017) — https://herbertograca.com/2017/11/02/ports-adapters-architecture/
- Herberto Graca, "DDD, Hexagonal, Onion, Clean, CQRS — How I Put It All Together" (2017) — https://herbertograca.com/2017/11/16/explicit-architecture-01-ddd-hexagonal-onion-clean-cqrs-how-i-put-it-all-together/