Hexagonal Architecture in Practice: What It Actually Buys You (And When It Doesn't)
"Hexagonal by default" has been sitting on my homepage's principles section for years now, and I realized recently I've never actually unpacked what that costs and what it buys, outside of a whiteboard diagram with a domain circle and some arrows pointing at boxes labeled "adapter." So this is that unpacking — not the ports-and-adapters tutorial, which you can find in a dozen places, but what fifteen years of defaulting to it has actually taught me, on real systems, with real deadlines.
Why It's the Default, Not a Preference
I didn't arrive at this from a book. I arrived at it from a message-broker platform at Spice Digital where the JMS API had quietly worked its way into the business logic of about forty classes, and swapping brokers — which we had to do, for reasons that had nothing to do with the code — turned into a six-week project instead of a configuration change. Nobody decided to couple the domain to the broker. It happened one convenient shortcut at a time, because there was no boundary forcing the question "does this class need to know it's talking to JMS?" That's the failure mode hexagonal architecture exists to prevent: not bad design in one sitting, but erosion over two years and six engineers who never met each other.
What It Actually Buys You
The honest answer is narrower than the pitch. It doesn't buy you fewer bugs, faster delivery, or easier code review on day one — if anything it costs you there. What it buys you is a specific, deferred option: the ability to change an infrastructure decision without touching domain logic, exercised months or years after the fact, when you have the least context and the least appetite to re-derive business rules from scratch.
On VisionWare+, the analytics platform I've been building at Apollo Supply Chain since
late 2023, the domain layer has no idea whether inventory events arrive over a message
queue, a scheduled poll, or a webhook — it depends on an inbound port, an interface
with a name like InventoryEventListener, and three different adapters have
implemented it over the platform's life as we changed how warehouses actually emit data.
Each swap was an afternoon: write the new adapter, wire it in Spring, delete the old bean.
Zero changes to the domain model, zero regression risk to the business rules that decide
what an inventory event actually means. That's the whole trade, made concrete.
You don't feel the value of a port on the day you write it. You feel it eighteen months later, on the day you don't have to touch forty classes to change one dependency.
The Bill Nobody Puts on the Estimate
None of that is free, and I don't pretend it is when I'm the one signing off on the estimate. Every port needs at least one adapter and usually a mapper between the persistence model and the domain model, which means a change that would be one class in a transaction-script style codebase is three or four here. I've watched a genuinely capable mid-level engineer lose forty minutes hunting for where a rule actually lives, bouncing between an interface, its Spring-wired implementation, and a DTO, because the codebase asks you to hold an extra layer of indirection in your head before anything makes sense. That's a real cost, and onboarding a team into a hexagonal codebase takes longer than onboarding one into a simpler layered structure — I plan for it explicitly now, with a walkthrough of exactly one vertical slice, port to adapter to domain, in every new engineer's first week.
A Before/After From the Warehouse Floor
The clearest example I have is carrier integration on the warehouse management system I
built for Xpdel and Zico in the US. Fulfillment logic — what counts as a valid
shipment, how partial fulfillment gets recorded, when an order is actually "shipped"
— lived entirely inside the domain, behind a CarrierGateway port. When
we onboarded a fourth carrier two years into production, the work was scoped, estimated,
and shipped as a new adapter implementing that same interface, translating that carrier's
particular API quirks into the same domain events every other carrier already produced.
No one touched fulfillment logic. No one re-tested the existing three carriers because
nothing about them changed. If that gateway hadn't existed as a seam, adding a carrier
would have meant finding and adjusting every place in the codebase that assumed carrier
"A" or "B" behavior, which is exactly the kind of change that quietly breaks something
unrelated three sprints later.
When I Skip It
I don't reach for this on everything, and I'd flag it as over-engineering if I saw it on the wrong project. Small internal CRUD tools, admin dashboards with a lifespan measured in one team's tenure, and the first ninety days of anything I haven't validated with real users yet all get a straightforward layered structure instead — controller, service, repository, done. The tell I use: if I can name the second real implementation a port will need within the next year, hexagonal earns its cost. If I can't name one, I'm buying optionality nobody's going to exercise, and that's ceremony, not architecture.
The Rule I Actually Use
Pattern discipline for its own sake has never been the goal — predictability under change is. Hexagonal architecture is the tool I reach for when I already know, from the shape of the domain or the volatility of the integrations around it, that something on the outside is going to move while the business rules underneath stay put. When I don't know that yet, I'd rather ship something simple and refactor toward a boundary once the volatility actually shows up, than pay the indirection tax speculatively on a system that never needed it.
← Back to all posts