core/architecture/hexagonal.md

Hexagonal Architecture (Ports & Adapters) in Depth

Concrete thresholds for adopting Ports & Adapters, named criticism of boilerplate-heavy misapplication, and how the pattern's testing payoff should be measured rather than assumed.

Assumes you already know primary/secondary ports, driving/driven adapters, and why the hexagon shape is arbitrary. This document covers when the pattern earns its keep, named criticism of its overuse, and the one metric that tells you whether it's working.

Decision Table: Adopt It or Don't

SignalAdopt HexagonalSkip it
External integration count3+ genuinely different external systems (DB, payment API, email, queue) that will plausibly be swapped or mocked1 database, nothing else
Test double needYou need to run business logic tests with zero live infrastructureTests already run fast enough against a real ephemeral DB
Multiple driving surfacesSame core logic needs to be invoked from HTTP + CLI + a queue consumerSingle HTTP entry point only
Team4+ engineers where adapter boundaries prevent stepping on each otherSolo/duo project

Practitioner sources converge on the same overengineering smell called out for Clean Architecture: "if the domain complexity of your problem is fairly low (CRUD-like), or if the challenge of your application is NOT in the complexity of its business rules," the ports-and-adapters split is ceremony without payoff. Applying it "by the book" regardless of problem shape is explicitly named as the failure mode, not the pattern itself.

Named Criticism of How It's Commonly Misapplied

  • Boilerplate inflation on small projects: practitioner write-ups
  • quantify the tax directly - over-abstraction in small projects can inflate boilerplate code by roughly 20%, from one-interface-per-adapter where only one implementation will ever exist. That interface adds an indirection hop for a swap that will never happen.

  • "Adapters" folder with no driving/driven distinction: the single most
  • cited real-world mistake is a flat adapters/ directory where nothing stops a driving adapter (HTTP router) from directly calling a concrete driven adapter (SQL repository), silently bypassing the port and making the "architecture" decorative rather than enforced. Fix with an import linter, not a naming convention alone.

  • Hexagonal used to justify service-per-adapter splits: a driven port
  • boundary is not automatically a network boundary. Treating "this needs an adapter" as "this needs its own microservice" is how teams back into a distributed monolith - see Microservices for the Uber DOMA and Segment case studies of exactly this over-splitting.

The One Metric That Tells You If It's Working

Symmetric testability is the entire value proposition - not a side effect. If, after adopting the pattern, your core business-logic tests still need a live database, live HTTP server, or network access to pass, the ports weren't drawn correctly (something concrete leaked through a port signature - an ORM row, a Request object, a boto3 response type). This is the fastest real-world audit: pick any core unit test, unplug the network, and see if it still passes.

Composition Point Discipline

Same non-negotiable as Clean Architecture's composition root: which concrete adapter satisfies which port is decided once, at bootstrap, never inside the core. A Depends() graph (FastAPI) or an explicit factory module both work; what fails is deciding it ad hoc inside a use case "just this once."

When Not to Use / Alternatives

DimensionHexagonal (Ports & Adapters)Clean ArchitectureSimple Layered / MVC
Primary organizing questionWho drives, who is driven?What are the business-rule rings?Framework convention
Best fitMany diverse external integrations, 4+ engineersDeep, evolving business-rule complexityCRUD-heavy, light domain logic
Quantified overuse cost~20% boilerplate inflation on low-complexity domains (practitioner estimate)Ring ceremony on trivial domains (see clean_architecture.md)N/A
Common misapplicationFlat adapters/ folder with no driving/driven enforcementInterface-per-implementation with one implementationBusiness logic in views/controllers

Choose Clean Architecture in addition to Hexagonal when the dominant complexity is inside the business rules themselves, not just at the integration edges - see Clean Architecture.

Choose plain MVC when there are few external integrations and thin business logic - the port/adapter ceremony has nothing to buy there.

Sources

  • Victor Rentea, "Overengineering in Onion/Hexagonal Architectures" - https://victorrentea.ro/blog/overengineering-in-onion-hexagonal-architectures/
  • Jeroen Rosenberg, "Simplifying Hexagonal Architecture: How to Avoid Excessive Boilerplate" - https://medium.com/jeroen-rosenberg/simplifying-hexagonal-architecture-how-to-avoid-excessive-boilerplate-48146005d34f
  • Java Code Geeks, "Hexagonal Architecture in Practice: Ports, Adapters, and Real Use Cases" - https://www.javacodegeeks.com/2025/06/hexagonal-architecture-in-practice-ports-adapters-and-real-use-cases.html
  • Three Dots Labs, "Is Clean Architecture Overengineering?" (shared criticism applies to Hexagonal) - https://threedots.tech/episode/is-clean-architecture-overengineering/