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:

TermDefinition
PortAn interface defined by the application — the named purpose of a conversation with the outside world
AdapterTechnology-specific translation code connecting an external actor to a port
Primary (Driving) portInbound interface — the application's API that external actors call (e.g., a use case interface)
Secondary (Driven) portOutbound interface — what the application needs from infrastructure (e.g., a repository interface)
Driving adapterInitiates interaction with the application (HTTP controller, CLI, test harness) — connects to a primary port
Driven adapterThe 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]
Confusion with Clean and Onion

All three architectures express the same foundational principle — dependency inversion toward the domain — with different vocabularies.

DimensionHexagonal (Cockburn, 2005)Onion (Palermo, 2008)Clean (Martin, 2012)
Layer NamesPort / Adapter / Application CoreDomain Model / Domain Services / Application Services / InfrastructureEntities / 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 CoreNothing — Hexagonal does not specify the internal structure of the application coreExplicit separation of Domain Services from Application ServicesExplicit 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.0

TypeScript — 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..");
}
Port Interface Placement Pitfall

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.

  • 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