Hexagonal Architecture — Port & Adapter Skill
Reconciled model (Clean Arch + DDD): driven ports are interfaces in the Domain, implemented by adapters in Infrastructure. On the driving side the entry point is an Application Service ([[cqrs-command]]); a driving interface is optional. The layer rules and the "interface optional" decision come from [[clean-arch-layer]].
When to use
Integrating an external service, defining a repository/gateway/notifier contract, writing an API client or DB access, adding a queue producer/consumer, or asking "is this a port or an adapter?".
Port design (normative)
- A port is an interface expressed in domain terms, owned by the inner layers.
- Name by domain intent, not technology:
OrderRepository, not PostgresOrderStore; PaymentGateway, not StripeClient.
- Port methods speak domain types (entities, value objects, IDs) — never
sql.DB, http.Request, ORM rows.
- Split by cohesive capability; many small ports beat one god-interface.
Driven vs driving
|
Driven (secondary / outbound) |
Driving (primary / inbound) |
| Direction |
The app calls it |
It calls the app |
| Examples |
Repository, PaymentGateway, EmailSender, EventPublisher |
HTTP handler, CLI command, gRPC service, message consumer |
| Interface |
In Domain, always |
App Service is the entry; interface optional |
| Implemented in |
Infrastructure |
Interfaces layer |
Adapter implementation — workflow
- Identify the domain need (what capability is required?).
- Define the driven port as an interface in the Domain, in domain types.
- Implement the adapter in Infrastructure (it depends inward on the port).
- Register the adapter at the composition root via dependency injection.
- Write adapter-specific tests against real infrastructure (testcontainers / in-memory).
Testing strategy
- Domain / application logic: unit tests with fake/mock adapters.
- Adapters: integration tests against real infra (testcontainers, in-memory DB).
- Ports: contract tests — one shared test suite every adapter of a port must pass (keeps swappability honest).
Anti-patterns to avoid
- Technology-specific types in port signatures (
sql.DB, http.Request, ORM entities).
- Adapter logic (retries, SQL, serialization) leaking into the domain.
- Skipping the port and coupling the app directly to a concrete client.
- One giant port; an interface per service "just because" on the driving side (see the optional-interface rule in [[clean-arch-layer]]).
Verification (executable)
The promise of hexagonal is swappability; prove it with a fitness function, not by inspection. Per-ecosystem tooling, a port/adapter checklist, and a contract-test pattern are in reference.md. Minimum: the Domain package imports zero infrastructure, and the app's unit tests run with no external dependencies.
Examples
A driven port (Domain) + two swappable adapters (Postgres + in-memory) + a contract test, in Go and TypeScript: examples.md.
1---2name: hexagonal-port3description: Hexagonal Architecture — Port & Adapter Skill4---5# Hexagonal Architecture — Port & Adapter Skill67> **Reconciled model (Clean Arch + DDD):** **driven ports** are interfaces in the **Domain**, implemented by adapters in **Infrastructure**. On the **driving** side the entry point is an **Application Service** ([[cqrs-command]]); a driving interface is **optional**. The layer rules and the "interface optional" decision come from [[clean-arch-layer]].89## When to use1011Integrating an external service, defining a repository/gateway/notifier contract, writing an API client or DB access, adding a queue producer/consumer, or asking "is this a port or an adapter?".1213## Port design (normative)1415- A **port** is an interface expressed in **domain terms**, owned by the inner layers.16- Name by **domain intent, not technology**: `OrderRepository`, not `PostgresOrderStore`; `PaymentGateway`, not `StripeClient`.17- Port methods speak **domain types** (entities, value objects, IDs) — never `sql.DB`, `http.Request`, ORM rows.18- Split by **cohesive capability**; many small ports beat one god-interface.1920## Driven vs driving2122| | Driven (secondary / outbound) | Driving (primary / inbound) |23| -------------- | ------------------------------------------------------- | --------------------------------------------------------- |24| Direction | The app **calls** it | It **calls** the app |25| Examples | Repository, PaymentGateway, EmailSender, EventPublisher | HTTP handler, CLI command, gRPC service, message consumer |26| Interface | **In Domain, always** | App Service is the entry; interface **optional** |27| Implemented in | Infrastructure | Interfaces layer |2829## Adapter implementation — workflow30311. Identify the domain need (what capability is required?).322. Define the **driven port** as an interface in the Domain, in domain types.333. Implement the **adapter** in Infrastructure (it depends inward on the port).344. Register the adapter at the **composition root** via dependency injection.355. Write **adapter-specific tests** against real infrastructure (testcontainers / in-memory).3637## Testing strategy3839- **Domain / application logic**: unit tests with fake/mock adapters.40- **Adapters**: integration tests against real infra (testcontainers, in-memory DB).41- **Ports**: **contract tests** — one shared test suite every adapter of a port must pass (keeps swappability honest).4243## Anti-patterns to avoid4445- Technology-specific types in port signatures (`sql.DB`, `http.Request`, ORM entities).46- Adapter logic (retries, SQL, serialization) leaking into the domain.47- Skipping the port and coupling the app directly to a concrete client.48- One giant port; an interface per service "just because" on the driving side (see the optional-interface rule in [[clean-arch-layer]]).4950## Verification (executable)5152The promise of hexagonal is _swappability_; prove it with a fitness function, not by inspection. Per-ecosystem tooling, a port/adapter checklist, and a contract-test pattern are in **[reference.md](reference.md)**. Minimum: the Domain package imports zero infrastructure, and the app's unit tests run with no external dependencies.5354## Examples5556A driven port (Domain) + two swappable adapters (Postgres + in-memory) + a contract test, in Go and TypeScript: **[examples.md](examples.md)**.