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
| Signal | Adopt Hexagonal | Skip it |
|---|---|---|
| External integration count | 3+ genuinely different external systems (DB, payment API, email, queue) that will plausibly be swapped or mocked | 1 database, nothing else |
| Test double need | You need to run business logic tests with zero live infrastructure | Tests already run fast enough against a real ephemeral DB |
| Multiple driving surfaces | Same core logic needs to be invoked from HTTP + CLI + a queue consumer | Single HTTP entry point only |
| Team | 4+ engineers where adapter boundaries prevent stepping on each other | Solo/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
- "Adapters" folder with no driving/driven distinction: the single most
- Hexagonal used to justify service-per-adapter splits: a driven port
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.
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.
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
| Dimension | Hexagonal (Ports & Adapters) | Clean Architecture | Simple Layered / MVC |
|---|---|---|---|
| Primary organizing question | Who drives, who is driven? | What are the business-rule rings? | Framework convention |
| Best fit | Many diverse external integrations, 4+ engineers | Deep, evolving business-rule complexity | CRUD-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 misapplication | Flat adapters/ folder with no driving/driven enforcement | Interface-per-implementation with one implementation | Business 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.
Related Documentation
- Clean Architecture
- Microservices Architecture
- SOLID Principles
- Unit Testing Principles
- Mocking Strategies
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/