Hexagonal architecture, ports and adapters, shares its core with the clean architecture discussion from earlier in this series, the dependency rule, inward pointing, business logic free of infrastructure concerns, and the difference between the labels is mostly geography and vocabulary. What the hexagonal framing adds is a specific emphasis, the ports are the designed interchange points, and the adapters are the swappable implementations behind them, and the pattern earns its separate article because the swap story is the one that gets tested against reality, the database migration that actually happened, the queue change that shipped, the payment provider that retired. This article builds the ports vocabulary, the driven and driving sides, the swap cases with their real timelines, the abstraction tax and where it is worth paying, and the test discipline that makes the whole pattern verifiable rather than aspirational.

The hexagon's two sides and the direction of the calls

The pattern's diagram is a hexagon, the domain inside, the ports on its edges, the adapters outside, and the two sides of the boundary have names worth keeping straight. On the driving side, the things that drive the application, the HTTP framework, the message consumer, the scheduled job, the command line tool, each one an adapter that translates its protocol into a port the domain defines:

# the port, defined by the domain, in the domain's language
class OrderCommandHandler:
    def handle(self, command: PlaceOrder): ...

# the driving adapter, translating HTTP into the port
@post("/orders")
def place_order(request, handler: OrderCommandHandler):
    result = handler.handle(PlaceOrder(**request.json))
    return respond(201, result)

On the driven side, the things the application drives, the database, the queue, the email sender, the payment client, each one behind a port the domain owns and an adapter that implements it outside. The domain module contains the interfaces and the logic, the adapters module contains the implementations, and the dependency arrows all cross the boundary inward, the adapter imports the port, never the reverse. The hexagon shape is merely a way to draw many ports around a core, and the word that carries the architecture is ports, the named interchange points, because a boundary without named ports is a boundary that leaks the first time a deadline arrives.

The swap scenarios that justify the vocabulary

The pattern's value proposition is the swap, and the honest catalog of real swaps is more varied than the database migration the literature always uses. The storage swap, PostgreSQL for the operations store, is the classic, and the swap that actually happens is usually softer, the test doubles that run the domain's full suite in milliseconds, which is a swap performed hundreds of times a day. The queue swap is real and recurring, the in process bus of the early system that becomes Redis streams or Kafka when the scale demands it, and the port that isolates it is the difference between a two week migration and a two quarter one. The provider swap is the commercial reality, the payment processor contract that ends, the email vendor that raises prices, the identity provider the acquiring company mandates, and each swap against a port is an adapter project, contained by design.

The swap that teams underrate is the protocol swap on the driving side, the REST endpoint that gains a gRPC sibling, the message consumer that also needs an HTTP webhook, the batch job that becomes an event handler, because the driving port already expressed the operation in domain language, the new adapter is a translation, not a reimplementation. The pattern's honest pricing, though, must include the swaps that never happen, the port written for a database that never changed, the adapter interface with one implementation across seven years, and the tax section below is where that cost is paid and weighed.

The abstraction tax and the leaks that survive it

Every port is a claim that the two sides behind it are interchangeable, and the claim is a tax paid in mapping code and in the discipline of keeping the abstraction honest. The tax is visible in every layer of translation, the domain's order identifier that becomes the database's integer, the queue's message that becomes the domain's command, and the codebases that feel buried in ceremony are usually paying the tax at every boundary rather than at the boundaries with real swap risk. The pattern's pragmatic setting is the same portfolio judgment the clean architecture dial made, full ports at the seams with volatility history, thin direct calls at the stable edges, and the difference between the two is a review decision recorded in the module's readme.

The leak is the tax's failure mode, the port that does not actually abstract. The repository port whose method signatures return ORM types, the payment port whose interface embeds the vendor's error taxonomy, the queue port that exposes the broker's delivery semantics, are ports that have already leaked, and the swap they promise is a rewrite wearing an interface. The diagnostic is the secondary adapter test, write a fake against the port without importing the concrete technology, and the fake that requires the technology's types or semantics proves the port is a decoration. The teams that keep their ports honest run that diagnostic in review rather than discovering it in the migration, because a leaked abstraction is discovered at exactly the moment it was supposed to save the project.

One swap walked through, the queue that grew up

A concrete swap prices the pattern better than an argument. The system ships with an in process publisher, a port that the domain calls and a trivial adapter that calls subscribed functions, and the code looks almost too simple to justify the interface. Eighteen months later the deployment splits into two services and the port that everyone doubted earns its keep, the new adapter publishes to Redis streams, the consumer side gains its own adapter, and the domain's publish calls do not change, because the port's signature was written in the domain's vocabulary, publish order placed with its payload, rather than in the transport's, send to topic with partition keys.

# the port stays, written the day one version
class EventPublisher:
    def order_placed(self, order_id: OrderId): ...

# the new adapter, the only file that knows streams exist
class StreamEventPublisher(EventPublisher):
    def order_placed(self, order_id):
        self.redis.xadd("events", {
            "type": "order.placed", "id": str(order_id)
        })

The swap that took a week included the operational education, the lag monitoring, the consumer group semantics, the dead letter path, and the codebase's role in it was two adapter classes and one wiring change in the composition root. The team that had called the port ceremony spent the same week writing publish calls into forty domain files, and the comparison is the pattern's value stated as a timeline, the interface that costs one line per operation versus the transport that costs one migration per change. The port did not predict the future, it priced the boundary correctly, which is all any abstraction is ever asked to do.

The testing discipline that makes the boundary real

The pattern's testing story is its most immediate return, and the payoff has two layers. The domain suite runs against ports with fakes, no database, no broker, no vendor, and it runs in milliseconds, so the business rules, the code the company owns, are exercised continuously, the same testing dividend the earlier architecture article priced. The adapter suite is thin and honest, it proves the wiring, the repository against a real database in a container, the payment adapter against the vendor's sandbox, and the combination is a suite that is fast where the logic lives and slow only where the glue lives, which is the shape a healthy test pyramid is always trying to reach.

def test_reservation_port_fake():
    store = FakeReservationStore()
    booking = BookingService(store)
    booking.reserve(Resource("room-7"), window)
    assert store.saved.reservations == 1

The port test with the fake is also the architecture's living documentation, the interface's contract demonstrated in code, and the new joiner reads the fakes to learn what the ports promise. The discipline that keeps the pyramid honest is the rule that the domain suite may not import the adapters package, enforced by the dependency checker rather than by memory, because architecture rules that live only in review are rules that expire with the reviewer's tenure.

The adapter tests that prove the wiring without the weight

The driving adapters get their own thin suite, and the shape is the same translation test the domain never sees. The HTTP adapter test builds the real route table with a fake handler behind the port, sends the actual HTTP request, and asserts two things, the translation into the port's command and the response serialization:

def test_place_order_endpoint():
    handler = FakeHandler()
    client = make_app(handler).test_client()

    resp = client.post("/orders", json={"sku": "SKU-9", "qty": 2})

    assert handler.commands == [PlaceOrder(sku="SKU-9", qty=2)]
    assert resp.status_code == 201

The test is a dozen lines, it runs in milliseconds, and it catches the entire class of adapter defects, the field name typo, the status code mistake, the validation the framework applied or did not. The driven adapters test against the real technology in a container, one suite per adapter, proving the SQL compiles, the stream publishes, the sandbox accepts, and the adapter test count is small by design, the domain carries the behavior and the adapter carries the connection, and connections need proof, not poetry. The combined suite structure is the architecture made executable, fast domain tests against fakes, thin adapter tests against realities, and the boundary between them is the same boundary the production wiring draws, which is the whole pattern summarized as a test strategy.

From here to a working hexagon without the ceremony

The implementation path that works in production systems is incremental and small. Define the domain's operations as ports first, commands and queries in the domain's own vocabulary, then the driven ports per external concern, storage, messaging, payments, then the adapters that implement them, and the composition wiring that this series discussed, the one file that knows every concrete class. The application does not need hexagons drawn in its package tree, it needs the imports to point inward and the ports to be named, and a flat package structure with disciplined imports delivers the pattern's entire value. The teams that over invest are recognizable by their folder ceremony, adapter and usecase and domain directories with one class each, and the teams that under invest are recognizable by their incident reviews, the vendor change that took a quarter because the vendor was everywhere.

The working rules compress the pattern to its operational core, and the list is short:

  1. Name the ports in the domain's language, and let the adapters translate, never the reverse;
  2. Write the fake against each port before the first real adapter, the fake is the port's test and documentation;
  3. Put full ports at the seams with swap history or swap risk, and call stable edges directly, recorded in the module readme;
  4. Enforce the import direction with a checker, the architecture that is not enforced is a diagram;
  5. Price the port per seam, and delete the interface that never earned its second implementation;
  6. Revisit the seams yearly, the volatility that justifies a port is a property of the market, not of the code.

The infrastructure will change, the vendors will merge and retire, the scale will demand different machinery, and the application that drew its interchange points on purpose meets each change as an adapter project inside a boundary it owns. The hexagon was never the shape, it was the ownership, and the ports are the architecture's way of writing down exactly where the company's code ends and the world's begins.