Home

/

The Production-Ready Playbook

/

Ports and Adapters (Hexagonal)

Ports and Adapters (Hexagonal)

Chapter 4
Part II
3
min read

Ports and Adapters (Hexagonal)

The problem: business logic that imports the database, the message broker, and the HTTP client is a knot you can't test in isolation or move to a new dependency.

Ports and Adapters (Cockburn, 2005, also called Hexagonal) draws one line. Inside it: your order domain, which defines ports, the interfaces it needs (IOrderGateway, IPaymentAdapter, IMapsService). Outside it: adapters, the concrete implementations that talk to the database, the payment provider, and the mapping API. The domain depends only on its own ports. Everything technical points inward, which is Uncle Bob's Dependency Inversion again at the level of a whole service.

// Inside the hexagon: a port the order domain owns.
public interface IPaymentAdapter { Task<PaymentResult> Charge(Money amount, CardToken card, CancellationToken ct); }

// Outside: an adapter the infrastructure layer provides.
public sealed class StripePaymentAdapter(StripeClient client) : IPaymentAdapter
{
    public Task<PaymentResult> Charge(Money amount, CardToken card, CancellationToken ct) => /* Stripe call */;
}

What it buys you in production: the order domain has no idea whether a charge goes to Stripe, Adyen, or an in-memory fake. You test order placement with fakes and never spin up a payment sandbox or a maps API. The day a new market needs a different payment provider, you write a new adapter and the domain doesn't notice.

Ports and adapters

Keep it minimal. The skip-if here is the ceremony, not the idea. You do not need the full Onion or Clean Architecture stack of four concentric layers, a separate assembly per ring, and a mapping class between every pair. For one service, the pattern is a folder of interfaces the domain owns and a folder of adapters that implement them. Add a ring only when a ring earns its keep.

Pipeline and Middleware

The problem: every request needs the same cross-cutting work (logging, auth, validation, error shaping), and copying it into each handler guarantees one of them drifts.

A pipeline composes that work into ordered stages, each wrapping the next. It's Chain of Responsibility (GoF) at the request level, and in .NET it's ASP.NET Core middleware: each component does its bit, then calls next. In the food-delivery API the ordered stages are tenant-resolution, auth, then logging, and order matters: you resolve the restaurant from the request before you check that the caller is allowed to act on it.

// Resolve the restaurant tenant from the subdomain or header, before anything else runs.
app.Use(async (ctx, next) =>
{
    var tenantId = TenantResolver.From(ctx.Request);   // e.g. host header -> tenant_id
    ctx.Items["tenant_id"] = tenantId;
    using (LogContext.PushProperty("tenant_id", tenantId))
        await next();                                  // auth and the rest run inside this scope
});

What it buys you in production: cross-cutting concerns live in one ordered place. Tenant resolution, auth, correlation IDs, and exception-to-problem-detail mapping become stages you add once and every request inherits. Order is explicit, so you can reason about what runs before auth and what runs after, and every log line downstream already carries the tenant_id.

The skip-if: don't build your own pipeline abstraction for two stages you could write as two method calls. The pattern pays off when the cross-cutting list is long enough that order and reuse matter. Below that, it's indirection you maintain for no gain.

the-pareto-stack-cloud-design-patterns-for-small-teams
the-ladder-of-altitudes
how-to-read-this
object-level-the-patterns-that-earn-their-keep
decorator
state
component-level-structuring-one-service
ports-and-adapters-hexagonal
mediator-the-commandquery-split
data-persistence
optimistic-concurrency
messaging-scale
outbox
resilience-staying-up-when-dependencies-dont
rate-limiting-throttling
timeout-fallback
the-composed-pipeline
observability-diagnostics-seeing-inside-production
metrics-the-four-golden-signals
externalised-configuration
hosting-cloud-agnostic-by-default
sidecar-ambassador
orchestrator-agnostic-deploy
a-reference-service
the-relay-outbox-to-queue
the-payment-saga-charge-pay-out-compensate
the-over-engineering-tax
conclusion-production-ready-deliberately
the-pattern-quick-reference-card
altitude-3-data-persistence
altitude-5-resilience
the-skip-list
full-event-sourcing-for-crud
robert-c-martin-uncle-bob-the-house-authority-for-structure
altitude-2-component
altitude-4-messaging-scale
altitude-6-observability-diagnostics

Download the full PDF for free?

Free download — no account required

Get the PDF
Get the PDF
Related Chapters
Free Download
Get the full PDF
All pages, including all code examples, diagrams, and the appendix reference card.
No spam. Unsubscribe at any time.
Your email won't be shared.
Oops! There's a problem with your request. We're working on fixing it. Please try again later.