Ports and adapters
In hexagonal architecture, a port is a domain-owned interface — the application defines it, not the infrastructure. An adapter is any component that implements or calls a port. This symmetry makes HTTP, CLI, and the database all 'just adapters' plugged into the domain's boundary.
The B2B order platform had three delivery channels at launch: an HTTP REST API for the web frontend, a CLI tool for the ops team, and a batch job that ran every night pulling orders from a partner’s SFTP drop. Each channel had its own copy of the order confirmation logic. When the product team added a billing approval step to the confirmation flow, three engineers updated three different code paths. Two of them got it right. The third missed the edge case for orders over $10,000. The bug lived in production for six weeks because nobody knew the CLI tool had its own confirmation logic. The core problem was not the three channels — it was that the application had no boundary. There was no single definition of “confirm an order” that every entry point called. When Alistair Cockburn described hexagonal architecture in 2005, this was exactly the problem he was trying to solve: the application needed a clear boundary that every caller — human, automated, or test — had to cross through the same interface.
What a port actually is
The word “port” in hexagonal architecture is borrowed from hardware: a port is a socket — a defined protocol for connecting. In software, a port is an interface that the application itself defines. This ownership is what distinguishes hexagonal architecture from layered architecture.
In the B2B order platform, the application might define a port like this:
// Defined inside the application — in the domain's own package
interface OrderConfirmationPort {
confirmOrder(orderId: string, approverId: string): Promise<ConfirmationResult>;
}This interface is owned by the application. It expresses what the application can do — in domain language, not in HTTP language or CLI language. The application defines the contract; the outside world must conform to it.
This is the inversion that distinguishes hexagonal from layered architecture. In a standard layered stack, the application layer calls OrderRepository, which is an infrastructure class — the application depends on infrastructure. In hexagonal, the application defines IOrderRepository as a port — infrastructure depends on the application’s interface definition. The dependency arrow runs from infrastructure toward the domain, never the other way.
What an adapter actually is
An adapter is any component that translates between the outside world and a port. An adapter either:
- Calls a port (the HTTP controller calls
OrderConfirmationPort.confirmOrderwhen a POST request arrives) - Implements a port (the
PostgresOrderRepositoryimplementsIOrderRepositoryto fulfil the domain’s data needs)
The critical insight is that these two categories are symmetric. The HTTP controller and the PostgreSQL repository are both adapters — one on the left side of the hexagon (calling in), one on the right side (called out). They are structurally equivalent: each one is a translation layer between an external technology and a port interface.
This symmetry is what makes the architecture’s name apt. The hexagon has faces, and each face can have a port. An HTTP adapter plugs into one face; a gRPC adapter could plug into the same face (using the same port). A PostgreSQL adapter plugs into a different face; an in-memory adapter could plug into the same face. The application core does not know or care which adapters are plugged in.
The symmetry: UI and database are both just adapters
This is the most practically useful consequence of hexagonal architecture. The HTTP layer is not privileged — it is just the adapter that translates HTTP requests into port calls. The database is not privileged — it is just the adapter that implements the repository port. Both are external to the application core.
This has an immediate structural consequence: you can swap either side independently. Swap the HTTP adapter for a gRPC adapter — the application core does not change. Swap PostgreSQL for DynamoDB — the application core does not change. Add a CLI adapter alongside the HTTP adapter — the application core does not change. This is exactly what the B2B order platform was missing: if the order confirmation logic had lived behind a single OrderConfirmationPort, the HTTP controller, CLI adapter, and batch job adapter would all have called the same port. One bug fix in one place.
▸Why this works
Why did Cockburn call it “hexagonal” rather than circular or square? The hexagon has no special significance — six faces is not a requirement. Cockburn chose the hexagon so diagrams would have room to draw multiple adapters on each side without the shape becoming too narrow. The meaningful shape is that the application is an enclosed area with explicit entry/exit points (the ports), not an open layer stack where anything can import anything. The number of faces is a drawing convenience, not an architectural constraint.
Ports as the application’s API and SPI
There are two categories of port, which map directly to the two sides of the hexagon:
Primary ports (API — Application Programming Interface) define how the outside world drives the application. They are the use cases the application exposes. In the B2B platform: OrderConfirmationPort, InvoiceGenerationPort, BillingApprovalPort. Driving adapters call these ports.
Secondary ports (SPI — Service Provider Interface) define what the application needs from infrastructure. The application calls these ports; infrastructure implements them. In the B2B platform: IOrderRepository, IPaymentGateway, IEmailNotifier, IEventPublisher. These are exactly the dependency-inverted interfaces from unit 02 — DIP applied at the architectural boundary.
The names API and SPI come from the Java ecosystem but the concept is universal. The application defines both sides: what it offers (API/primary ports) and what it requires (SPI/secondary ports). The outside world must conform to both.
▸lesson.inset.note
A common mistake is to think “the port is the HTTP endpoint” or “the port is the database table.” Neither. Ports are defined in domain language. OrderConfirmationPort.confirmOrder(orderId, approverId) speaks order domain language — it knows nothing about HTTP verbs, JSON bodies, SQL tables, or message queue topics. The translation between HTTP-speak and domain-speak is exactly the job of the adapter. This separation — domain language at the port, technology language in the adapter — is what makes the application testable without HTTP infrastructure and replaceable without domain rewrites.
The team defines IOrderRepository in the infrastructure package alongside PostgresOrderRepository. The domain's OrderService imports it from there. Is this hexagonal architecture, and why or why not?
The B2B platform has an HTTP controller, a CLI adapter, and a batch job adapter — all three activate the order confirmation use case. How should these adapters relate to the OrderConfirmationPort in hexagonal architecture?
After applying hexagonal architecture to the B2B order platform, the team wants to replace PostgreSQL with a document store for the orders collection. Which components change, and which stay the same?
- 01What makes a port a 'port' in hexagonal architecture — how is it different from any other interface?
- 02The B2B platform had three adapters (HTTP, CLI, batch) each with their own copy of confirmation logic. How does hexagonal architecture fix this structurally?
- 03What are primary ports and secondary ports, and which side of the hexagon does each sit on?
Hexagonal architecture solves the problem of the application having no clear boundary — where every entry point contains its own copy of the business logic, and a single bug fix must be replicated across multiple codebases.
A port is an interface owned by the application. It speaks domain language and defines either what the application offers (primary port) or what it requires from infrastructure (secondary port). The domain writes the contract; the outside world must conform to it.
An adapter is a translation layer between an external technology and a port. Driving adapters (HTTP controllers, CLI runners, batch jobs, test drivers) call primary ports to activate the application. Driven adapters (PostgreSQL repositories, Stripe payment clients, SendGrid email senders) implement secondary ports to fulfil the domain’s infrastructure needs.
The symmetry of this arrangement — that HTTP and PostgreSQL are both just adapters on different sides — is the practical payoff. Swap the HTTP adapter for gRPC, the application core does not change. Swap PostgreSQL for a document store, the application core does not change. Add a new entry point, the confirmation logic does not duplicate. The application core is an enclosed area; the ports are its only legal entry and exit points; the adapters are the conforming connectors the outside world provides.
This architecture is the structural answer to the layering leaks from unit 03. The transaction-script trap is prevented because the port forces a defined use-case boundary. Layer-skipping is prevented because infrastructure can only reach the core through port implementations — infrastructure depends on the domain’s interfaces, not the other way around. The fat service is prevented because each primary port corresponds to a bounded use case, not an open method list.
Practice
Start at the top. Tasks go easiest → hardest: recall a fact, apply it to a case, then a senior-level stretch. Open one, attempt it, then reveal.
Something unclear?
Ask a question about this lesson. Questions are anonymous and go straight to the author to make the lesson better.
Apply this
Put this lesson to work on a real build.