Ports and Adapters Architecture
Also called "Hexagonal Architecture" by Alistair Cockburn.
Core Idea
Isolate your business logic in the center ("hexagon"). Everything outside—databases, web frameworks, message queues, UI—connects through explicit ports and adapters. This inversion of dependencies makes business logic reusable and testable.
Structure
hexagon/
├── domain/ ← Business logic (center), no dependencies on outside
├── ports/
│ ├── driving/ ← Interfaces for external actors to call in
│ │ └── UserService.ts
│ └── driven/ ← Interfaces for the domain to call out
│ └── UserRepository.ts
├── use-cases/ ← Application layer, orchestrates domain
│ └── CreateUserUseCase.ts
adapters/
├── driving/ ← How external actors call the domain
│ ├── http/ ← REST controller
│ └── cli/ ← Command-line interface
└── driven/ ← How the domain calls external systems
├── postgres/ ← Concrete database implementation
├── email/ ← Email service implementation
└── cache/ ← Cache service implementation
Key Concepts
| Term | Meaning |
|---|---|
| Port | Interface defined by the hexagon (domain). External systems implement it. |
| Adapter | Concrete implementation of a port. Translates between domain logic and external tools. |
| Driving / Primary | Ports where external actors (HTTP client, CLI user, scheduler) call into the domain. |
| Driven / Secondary | Ports where the domain calls out to external systems (database, cache, email). |
Naming convention: For driving ports, use for_<action> (e.g., for_creating_users).
Advantages
- Business logic is framework-agnostic — swap HTTP for gRPC without touching the domain
- Testability: inject fakes for driven ports; test domain in isolation
- Clarity: dependencies flow inward only; domain never depends on adapters
- Scalability: add new adapters (new databases, new APIs) without changing the core
Application Workflow
When the user wants to apply or review Ports & Adapters:
- Identify the boundary — where does business logic end and infrastructure begin? The hexagon edge is the answer.
- Define the ports — write interfaces (not implementations) for every outbound dependency. Name them
for_<action>. - Move logic inward — ensure domain code imports only other domain code, never framework/DB packages.
- Write adapters — one concrete class per external system, implementing the matching port interface.
Read On Demand
| Read When | File |
|---|---|
| Before/after TypeScript example showing the refactoring | Before / After Example |
| Ownership tables + tool-enforced boundaries from real NestJS hex repos | Ownership & Boundary Enforcement |
Read On Demand (External)
- Alistair Cockburn — Hexagonal Architecture
- Martin Fowler — Ports and Adapters Pattern
Functional Programming Variants (Haskell, OCaml, Erlang, Elixir, Clojure, ...)
Ports & Adapters maps onto FP languages as Functional Core, Imperative Shell: a pure, side-effect-free core (the hexagon) surrounded by a thin shell that performs I/O and calls the core with plain data in, plain data out. No classes/interfaces needed for ports — plain functions (or typeclass/protocol dispatch) fill that role.
- Janet A. Carr — Ports and Adapters Architecture for the Functional Programmer
- Gary Bernhardt — Functional Core, Imperative Shell (screencast)
- functional-architecture.org — Functional Core, Imperative Shell
- Community notes doc — Functional Core / Imperative Shell — shared notes
Benchmark
Scenario: .benchmarks/scenarios/ports-adapters-001-hexagonal-refactor.md · Run: 2026-08-31 · Log: .benchmarks/runs/2026-08-31/ports-adapters-001-hexagonal-refactor.json
| Model | Without | With | Delta |
|---|---|---|---|
| claude-opus-4-8 | 100% | 100% | +0% |
| claude-sonnet-4-6 | 100% | 100% | +0% |
| claude-haiku-4-5 | 100% | 100% | +0% |
NEUTRAL (run 2026-08-31). All models 100% with and without — hexagonal refactor at ceiling. The port-naming BLOCKER did not bite here but still needs the Phase-7 scoping fix. Gate per
.agents/skills/skill-optimizer/rules/release-gates.md.