Ports and Adapters Architecture: How Hexagonal Design Works
Ports and Adapters, commonly called Hexagonal Architecture, is a way to keep an application’s important rules independent of the technologies around it. It is useful when a system needs to support more than one interface or external service, or when business behavior should be testable without starting a database or web server. The central design decision is to make the application communicate through purposeful boundaries rather than depend directly on infrastructure.
What Is Ports and Adapters Architecture?
Ports and Adapters is an application architecture in which the application core defines the interactions it needs and offers, while adapters translate those interactions to and from external technologies. A port is a contract for a purposeful conversation; an adapter is a technology-specific implementation or translation at that boundary.
The application core contains the use cases and domain rules. An HTTP controller, command-line interface, scheduled job, database, message broker, or third-party API belongs outside that core. The shape is often drawn as a hexagon, but the number of sides has no technical significance. Alistair Cockburn’s original Hexagonal Architecture report describes the goal as allowing an application to work with different users, tests, and devices without coupling its behavior to them.
The Problem It Solves
When application rules are mixed into web handlers, SQL queries, or vendor SDK calls, changing one of those technologies can force changes throughout the codebase. The same coupling makes a use case difficult to exercise in isolation: a test may need to launch infrastructure merely to check a business decision.
Ports and Adapters addresses this by separating two concerns:
- What the application does: use cases and domain rules, expressed without a dependency on a specific web framework or storage engine.
- How the application connects to the outside: adapters that translate requests, persist data, or call remote services.
This does not make infrastructure disposable or eliminate integration testing. It makes the boundary explicit, so that core behavior can be tested with small fakes and each real adapter can be tested against its actual technology.
How It Works: Ports, Adapters, and Dependencies
A typical request flows from an external system through an adapter into a use case, then out through another port to an adapter:
HTTP request -> HTTP adapter -> inbound port -> application use case
|
v
Database <- SQL adapter <- outbound port <------+
There are two common port directions:
- Inbound (driving) ports describe work the application allows a caller to request, such as
CreateNoteorTransferFunds. An HTTP controller or CLI adapter invokes the port. - Outbound (driven) ports describe capabilities the application needs, such as
NoteRepository,Clock, orPaymentAuthorizer. The core defines the contract; an external adapter implements it.
The runtime call direction and source-code dependency direction are not always the same. A use case calls an injected repository object at runtime, but its code imports only the repository contract. A SQL adapter depends on that contract to implement it. As a result, a database driver can change without requiring the use case to import a new driver.
The core is not required to contain every piece of code. Adapters can have their own mapping, retry, serialization, and protocol logic. The boundary should keep those details out of domain decisions, and the port should express application intent rather than expose a vendor’s API.
Components and Architectural Variants
| Approach | Main boundary | Typical dependency rule | Useful when |
|---|---|---|---|
| Layered architecture | Presentation, business, and data layers | Often flows from presentation toward lower layers | A straightforward application benefits from clear responsibility tiers |
| MVC | Model, view, and controller responsibilities | Organizes interaction and presentation concerns | The main design question is how to structure a user interface |
| Ports and Adapters | Application core and external actors | Core code depends on its own port contracts, not adapter implementations | A use case needs multiple interfaces or replaceable infrastructure |
| Clean Architecture | Concentric policy boundaries around business rules | Source dependencies point toward inner policies | A system needs a broader set of rules for organizing modules and layers |
These approaches are not mutually exclusive. An application can use MVC in its HTTP adapter and organize its use cases using Ports and Adapters. Clean Architecture shares the inward dependency principle, but it is a broader family of architectural guidance rather than another name for every hexagonal design. Microsoft’s web application architecture guidance also explains the role and trade-offs of separating responsibilities into layers.
The main building blocks are the application core, port contracts, adapters, and a composition root. The composition root is the startup code that creates concrete adapters and injects them into the core. Dependency injection is a common way to perform that wiring, but a framework container is not required.
Real-World Use Cases
- A service with multiple entry points: expose the same use case through an HTTP API, a batch job, and an administrative CLI without copying business rules into each interface.
- A system integrating with external providers: isolate payment, email, or identity-provider APIs behind ports so provider-specific request formats remain in adapters.
- A product with evolving storage needs: keep application logic independent of a particular database, while validating each persistence adapter with integration tests.
- A codebase with valuable domain rules: run fast unit tests against the core and reserve slower tests for database, network, and framework behavior.
These boundaries are useful even in a single deployable application. They do not require microservices, distributed deployment, or a large number of modules.
Practical Considerations: A Small TypeScript Example
Consider a note-taking use case. The inbound port exposes the action, while the repository and ID generator are outbound ports. The application service contains validation and depends only on those contracts:
export interface Note {
id: string;
title: string;
body: string;
}
export interface CreateNoteCommand {
title: string;
body: string;
}
export interface CreateNote {
execute(command: CreateNoteCommand): Promise<{ id: string }>;
}
export interface NoteRepository {
save(note: Note): Promise<void>;
}
export interface IdGenerator {
next(): string;
}
export class CreateNoteService implements CreateNote {
constructor(
private readonly notes: NoteRepository,
private readonly ids: IdGenerator,
) {}
async execute(command: CreateNoteCommand): Promise<{ id: string }> {
const title = command.title.trim();
if (!title) {
throw new Error('A note title is required');
}
const note: Note = {
id: this.ids.next(),
title,
body: command.body.trim(),
};
await this.notes.save(note);
return { id: note.id };
}
}
An HTTP adapter would parse and validate the transport-level request, call CreateNote.execute, then map the result to an HTTP response. A SQL adapter would implement NoteRepository; an in-memory fake can implement the same interface in a unit test. For example, with a test runner that provides expect:
const saved: Note[] = [];
const service = new CreateNoteService(
{ save: async (note) => { saved.push(note); } },
{ next: () => 'note-1' },
);
const result = await service.execute({ title: ' Draft ', body: ' Notes ' });
expect(result).toEqual({ id: 'note-1' });
expect(saved[0]).toEqual({ id: 'note-1', title: 'Draft', body: 'Notes' });
At startup, construct CreateNoteService with the real repository and ID generator, then give the service to the HTTP adapter. Keep this wiring at the edge instead of letting the use case create its own database client. Frameworks can help manage dependencies, and the Spring Framework testing reference describes how inversion of control supports unit and integration testing.
For a maintainable implementation:
- Name ports after application capabilities, not technologies.
NoteRepositorycommunicates intent more clearly thanPostgresNoteRepositoryin the core. - Keep the port narrow. Do not mirror an entire vendor SDK or expose database-specific query objects unless the application truly needs them.
- Test core rules with fakes, then test each adapter’s translation and failure behavior with focused adapter or integration tests.
- Put transactions and consistency decisions at an explicit boundary. Replacing a port does not make a multi-service workflow atomic; remote calls still need timeouts, idempotency, and recovery strategies where appropriate.
- Use the pattern where an independent boundary pays for its extra contract and wiring. A one-off script with no meaningful business rules may not need several interfaces and modules.
Common Misconceptions
- “The diagram must have six sides.” The hexagon is a drawing convention, not a required number of ports or modules.
- “Every interface is a port.” A port represents a meaningful conversation across the application boundary. Interfaces used only for internal organization do not automatically create architectural boundaries.
- “Adapters are always interchangeable.” Two adapters can implement the same contract and still differ in latency, consistency, limits, and failure modes. Those differences need tests and operational handling.
- “The pattern means no framework or database can appear anywhere.” Frameworks and databases are normal at the edges. The goal is to prevent their details from controlling core policies, not to ban them.
- “Ports and Adapters guarantees testability or good design.” A badly chosen port can simply move coupling behind an abstraction. The core still needs cohesive use cases, meaningful contracts, and tests at the right boundaries.
- “It requires microservices or a rewrite.” It can be introduced incrementally around one use case in a monolith, and it does not prescribe deployment topology.
Related Articles
- Dependency injection techniques explain how to provide concrete adapters to application services.
- Building extensible software systems covers plugin and extension boundaries that can use similar separation.
- Building resilient distributed systems explores how to handle failures across the external services that adapters connect to.

