· software-engineers Editorial · Career · 5 min read
Clean Architecture Hexagonal Ports Adapters
Hexagonal architecture in 2026: ports, adapters, and dependency rules that keep domain logic testable and framework-agnostic.
Why Hexagonal Architecture Still Matters in 2026
Hexagonal architecture (also called ports and adapters, coined by Alistair Cockburn in 2005) solves a problem that has only gotten worse as systems fan out into microservices, serverless functions, and AI-augmented pipelines: how do you keep business logic from rotting under the weight of frameworks, databases, and third-party SDKs?
The core idea is simple. Your domain logic sits at the center of a hexagon. It talks to the outside world only through ports — interfaces it defines and owns. Everything external — a REST controller, a Postgres repository, a Kafka consumer, an OpenAI client — is an adapter that implements or calls those ports. The dependency arrow always points inward. Nothing in your domain imports Express, Prisma, or any SDK.
In a July 2026 codebase, this pattern is no longer optional polish. Teams shipping LLM-integrated features are swapping model providers (Anthropic, OpenAI, self-hosted Llama variants) every quarter as pricing and quality shift. If your domain logic calls an SDK directly, every provider swap touches business code. If you defined a CompletionPort interface instead, you swap one adapter file.
The Core Building Blocks
Domain layer: entities, value objects, and use cases. Zero framework imports. Pure functions and plain classes.
Ports: interfaces defined by the domain, expressing what it needs (UserRepositoryPort, NotificationPort, PaymentGatewayPort). Ports are technology-agnostic — they describe intent, not implementation.
Driving adapters (primary/inbound): the things that call into your application — HTTP controllers, CLI commands, message queue consumers, scheduled jobs, GraphQL resolvers.
Driven adapters (secondary/outbound): the things your application calls out to — database repositories, external API clients, file storage, email providers.
Application/use-case layer: orchestrates domain entities and calls ports, without knowing which adapter is plugged in at runtime. Dependency injection wires the concrete adapter in at the composition root.
A Practical Example: Order Fulfillment Service
Consider an order fulfillment use case. The domain defines OrderRepositoryPort, InventoryPort, and NotificationPort. The use case FulfillOrderUseCase depends only on these interfaces:
class FulfillOrderUseCase {
constructor(
private orders: OrderRepositoryPort,
private inventory: InventoryPort,
private notifier: NotificationPort
) {}
async execute(orderId: string): Promise<void> {
const order = await this.orders.findById(orderId);
if (!order) throw new OrderNotFoundError(orderId);
await this.inventory.reserve(order.items);
order.markFulfilled();
await this.orders.save(order);
await this.notifier.send(order.customerId, "order_fulfilled");
}
}
At the composition root, you inject PostgresOrderRepository, RedisInventoryAdapter, and TwilioNotificationAdapter. Swap Postgres for DynamoDB and this use case file never changes — you only touch the adapter and the wiring.
This is the single biggest interview signal senior engineers look for: can you draw the boundary between “what the business needs” and “how we currently implement it”? Candidates who can sketch this on a whiteboard in under five minutes consistently outperform in system design rounds. The 0-to-1 SWE Interview Playbook (https://www.amazon.com/dp/B0H256Z1MF?tag=sirjohnnymai-20) walks through exactly this kind of boundary-drawing exercise across a dozen worked interview scenarios.
Testing Benefits: Why This Pays Off Immediately
Because use cases depend only on ports (interfaces), unit tests substitute in-memory fakes instead of spinning up a real database or hitting a real API:
const fakeOrders = new InMemoryOrderRepository();
const fakeInventory = new FakeInventoryAdapter();
const spyNotifier = new SpyNotificationAdapter();
const useCase = new FulfillOrderUseCase(fakeOrders, fakeInventory, spyNotifier);
No mocking frameworks fighting against tightly coupled constructors. No test suite that takes 40 seconds to boot a database container for a single assertion. Teams that adopt this pattern report unit test suites running 8-15x faster than integration-style tests that hit real infrastructure, because the domain layer has zero I/O.
Hexagonal vs. Layered vs. Clean Architecture
| Aspect | Traditional Layered (N-tier) | Clean Architecture (Uncle Bob) | Hexagonal (Ports & Adapters) |
|---|---|---|---|
| Dependency direction | Top-down, often leaky | Strictly inward toward entities | Strictly inward toward domain |
| Framework coupling | High — controllers often call ORM directly | Low — use cases isolated | Low — ports abstract everything |
| Terminology | Presentation/Business/Data | Entities/Use Cases/Interface Adapters/Frameworks | Domain/Ports/Adapters |
| Swap infra without touching logic | Hard | Moderate | Easy — that’s the entire point |
| Onboarding curve | Low | Medium-high | Medium |
| Best fit | Small CRUD apps, short-lived MVPs | Large monoliths with complex business rules | Systems with volatile external dependencies (multi-provider APIs, event buses) |
| Testing story | Requires mocking ORM/framework classes | Requires interface mocking at multiple layers | Trivial — inject fakes at one boundary |
Common Pitfalls Teams Hit in 2026
Leaky ports. A port that returns a database-specific type (a Prisma model, a Mongoose document) isn’t really a port — it’s a database adapter wearing a costume. Ports must return domain types.
Anemic domain models wrapped in ceremony. Hexagonal architecture adds files and indirection. If your “domain” is just getters and setters with no behavior, you’ve added complexity without benefit. Only invest in this pattern when business rules are non-trivial.
Over-abstracting single-implementation ports. If you will genuinely never swap Postgres, an interface with exactly one implementation forever is pure ceremony. Reserve ports for genuinely volatile boundaries — payment providers, LLM vendors, notification channels, third-party integrations that change on business terms outside your control.
Anemic composition roots. Wiring should happen in one place (a main.ts, a DI container config, a bootstrap module) — not scattered across the codebase with adapters instantiating each other.
FAQ
Q: Is hexagonal architecture overkill for a small startup MVP? A: For a true throwaway prototype, yes — skip it. But if you expect the product to survive past six months, or you’re integrating with LLM providers likely to change (a near-certainty in 2026), the port abstraction around external dependencies pays for itself within the first provider swap.
Q: How does hexagonal architecture relate to dependency injection? A: DI is the mechanism; hexagonal architecture is the design philosophy. Ports are interfaces, and DI containers (or manual composition roots) are how you wire concrete adapters into use cases at runtime without the domain layer knowing which adapter it received.
Q: What’s the difference between a port and a plain interface? A: Technically none in code — a port is just an interface. The distinction is conceptual: ports are defined by and owned by the domain to express what it needs from the outside world, not what a specific technology offers. This ownership direction (domain defines the contract, infrastructure implements it) is what keeps the dependency arrow pointing inward.
Interviewers in 2026 increasingly probe for this exact judgment call — knowing when hexagonal architecture is worth the ceremony versus when it’s resume-driven over-engineering is a stronger signal than reciting the pattern definition. Practice articulating both sides before your next system design round.