Most introductory articles draw it as an onion
Every article about Hexagonal Architecture ends up with the same diagram: three concentric circles. Domain on the inside, Application in the middle, Infrastructure on the outside. Onion Architecture. Clean Architecture. Layered Architecture with a different hat.
This is not what Ports & Adapters is.
The difference is not in the number of layers—it's in the direction of coupling.
In a layered architecture, dependency flows vertically: Controller calls Service, Service calls Repository. Each layer depends on the one below it. The problem is not that this doesn't work—it works fine for many projects. The problem is that business logic and infrastructure still couple in the same direction. Your UserService calls UserRepository (an interface), but the implementation carries JPA annotations, transaction annotations, and caching annotations. Testing the Service layer still means dealing with these implicit dependencies.
In Hexagonal Architecture, dependency flows radially inward: Adapters depend on Ports, Ports belong to the Domain. There is no "upper layer" and "lower layer"—only "inside" and "outside."
// Layered Architecture: dependency flows downward
Controller → Service → Repository (impl) → JPA/MySQL
// Hexagonal Architecture: dependency flows inward
Controller → [Port In] ← ApplicationService → [Port Out] ← Adapter
↓
DomainService → [Port Out] ← Adapter
The key difference: in layered architecture, the Service layer depends on the Repository implementation directly (even through an interface, the impl lives in the same layer). In hexagonal architecture, the business logic has no knowledge of who implements Port Out. It could be MyBatis, an in-memory Map, or a gRPC call. This "not knowing" is Dependency Inversion applied at the architectural level, not just the class level.
Alistair Cockburn's original 2005 article described two "ports" (inbound and outbound) with "adapters" translating between the port protocol and whatever technology sits outside. There were exactly two ports, not three, four, or five layers. The proliferation of inner rings in modern interpretations comes from Clean Architecture's influence, not from Cockburn's original model. This matters because each additional ring creates a new boundary where objects must be mapped—and each mapping is a cost.
Why not another architecture?
I evaluated three alternatives before settling on Ports & Adapters. Each had a different cost structure.
Layered architecture is the natural starting point. The project began this way and shipped in three days. But as the codebase grew, the problem wasn't "does it work"—it was "how much code must I read to make a change." In a layered architecture, a feature crosses three layers with no formal contract between them. Adding a field means verifying the Controller DTO, the Service parameter, and the Mapper mapping all changed. Nothing prevents a Controller from injecting a Mapper directly—and code review missed it multiple times. Layered architecture's constraints are soft constraints. They depend on team discipline in an environment where frameworks like Spring actively encourage cross-layer injection through @Autowired.
Clean Architecture shares the same inward-dependency topology as Hexagonal, but introduces more named layers (Entities, Use Cases, Interface Adapters, Frameworks). The naming imposes a cognitive cost: "why is this class an Entity but that one isn't?" "What's the boundary between a Use Case and a Domain Service?" Every team member needs to internalize these distinctions before they can make consistent architectural decisions. For a project that was already running, introducing Clean Architecture meant introducing terminology debates before we could fix the actual coupling problems. Hexagonal's vocabulary is smaller—port, adapter, domain—and leaves the internal structure of the hexagon as an implementation detail rather than prescribing it.
CQRS + Event Sourcing is appropriate when the read/write ratio is extreme or when audit requirements demand a complete event log. Neither condition held for this project. The read and write models share the same shape, and the cost of maintaining a separate event store would have exceeded its value.
Hexagonal Architecture won for three project-specific reasons:
Multiple inbound protocols, multiple outbound technologies. The project doesn't just serve HTTP requests—it streams via SSE, receives Stripe webhooks, authenticates Chrome extension devices. It doesn't just use a database—it calls translation engines (HTTP), searches vectors (Redis), processes payments (Stripe), sends emails (SMTP). Hexagonal treats "inbound" and "outbound" as first-class architectural concepts. Adding a protocol means adding an adapter, not modifying business logic.
The translation engine is the most volatile dependency. Translation providers change frequently—vendor switches, A/B tests, local vs cloud deployment. If engine selection leaks into business code, every switch becomes surgery across multiple files. Port Out + Adapter turns "switch engine" into "swap adapter."
Testing business logic doesn't need infrastructure. The translation pipeline depends on Python services, vector search depends on Redis, payments depend on Stripe SDK. In layered architecture, testing the Service layer means starting or mocking all of these. In hexagonal architecture, domain tests mock Port Out interfaces—no Spring context, no Redis, no WireMock. This matters in CI: domain tests run without any external service.
This is not a universal recommendation. If your project has one inbound protocol (a standard HTTP API) and one outbound technology (a single database), layered architecture has a lower overhead cost and clearer payoff. Hexagonal's interface count and adapter classes are pure overhead in this scenario.
The direction of a Port determines the architecture's nature
Many articles describe Ports as "just interfaces" where Inbound means "called by Controller" and Outbound means "implemented by Repository."
The critical property of a Port is not "it's an interface"—it's who owns it.
-
Port Inbelongs to the Application layer—it defines "what the system can do," implemented by ApplicationService. -
Port Outbelongs to the Domain layer—it defines "what the domain needs," implemented by Adapters.
This is Dependency Inversion rendered at the architectural level. The domain defines the interfaces it needs; infrastructure implements them.
I misunderstood this twice. The first time, I treated Port as "a place to define interfaces" without distinguishing who defined them. The second time, I placed Port Out inside the adapter package, thinking "domain-owned outbound interfaces" was DDD dogma. The practical consequence: when an interface needed to change, I couldn't tell whether the business requirement had changed (Port Out change means the domain's contract with the outside world changed) or the implementation had changed (adapter-only change, no business impact).
Port ownership determines change impact scope. Changing a Port Out signature means the domain's expectation of the outside world has changed—not just that a repository implementation has been swapped.
Domain layer "zero dependency" is a matter of degree
Hexagonal Architecture advocates often say "the domain layer has zero framework dependencies." Strictly speaking, this is a matter of degree, not an absolute.
Does using java.time.LocalDateTime count as a framework dependency? Technically yes (JSR 310), but nobody cares about this level. Does using Lombok @Data count? It's an annotation processor with no runtime impact—most projects accept it.
But using Spring @Transactional, Jackson @JsonProperty, or Hibernate @Entity introduces runtime behavior changes that require framework context for testing. These should not appear in the domain package.
Our domain layer uses Lombok and nothing else. No @Service, no @Entity, no @Value. The test is not "does it have annotations" but "can the domain logic still compile and test if the framework is removed from the classpath."
Adapters are not just translators—they are security boundaries
The conventional view of an Adapter is protocol translation: HTTP request to Java call, Java object to SQL statement, Java exception to HTTP status code.
Adapters have an implicit second responsibility: input sanitization and exception classification.
External systems are untrusted. A translation engine can be DNS-hijacked. Stripe callbacks can be replayed. Cached data in Redis can have the wrong format (cross-version deployment).
If an adapter only translates protocols without validating inputs, the problem propagates to the domain layer. And the domain layer, by design, knows nothing about the outside world—making it poorly equipped to distinguish "valid data" from "data that happens to be valid-looking."
The boundary between adapter validation and domain validation follows a useful heuristic:
- Format validation (JSON syntax, field type, non-empty string) — adapter layer.
- Semantic validation (order amount must be positive, status transition must be legal, user cannot delete self) — domain layer.
If the validation rule remains valid after replacing the technology implementation, it belongs in the domain layer.
Code boundaries are not deployment boundaries
Hexagonal Architecture solves a code organization problem: the dependency topology between different concerns.
It does not solve a deployment problem: all adapters and the domain layer compile into one JAR, run in one JVM, and share memory and thread pools.
If one adapter leaks memory (loading all translation cache into the process), it affects the entire application. If one adapter's HTTP connection pool is exhausted, it blocks other adapter requests.
This is not a flaw in Hexagonal Architecture—it never promised to solve deployment isolation. But in a containerized environment, this is a separate concern that must be designed independently. Code boundaries and process boundaries can align or diverge depending on deployment strategy, not code structure.
The security semantics of Converters
Domain models and persistence entities are separate classes, connected by Converters. This is not redundancy—it's isolation.
Domain models reflect business concepts; persistence entities reflect database structure. They can legitimately differ—the entity may store concatenated values that the domain splits apart, or the domain may use enums while the database stores integers.
Converters have a property that is easy to overlook: different directions have different safety constraints.
Converting from persistence entity to domain model requires validation—the data may have been written by an older version that accepted values now considered invalid. Converting from domain model to persistence entity assumes domain constraints have already been enforced—validation should not be delegated to the converter layer.
How to tell when Hexagonal Architecture is overkill
Small projects don't need it. Not because it's wrong for small projects, but because the ROI scales with complexity.
When your system has 3 Controllers, 5 Services, and 2 tables, Hexagonal Architecture's indirection is a concrete cost with an abstract benefit. Maintaining six files (Port interface, ApplicationService, Domain Model, Adapter, Converter, Entity) is measurably more expensive than maintaining two files (Controller, Service). The benefit—future-proofing against change—is speculative.
When the system grows to multiple inbound protocols, multiple outbound technologies, and a complex domain, the clear dependency boundaries start paying back. Not because there's more code, but because the cost of reasoning about a change decreases. When you know "translation engine changes go to adapter/out/translate" and "billing logic changes go to domain/service," you don't need to read the entire codebase to assess impact.
This is a threshold problem. The threshold depends on team size, change frequency, and technology stack complexity. There is no universal answer.
Summary
Hexagonal Architecture is not layered architecture rearranged. It's a different dependency topology—radially inward instead of vertically downward.
Why choose it? Not because layered architecture is wrong, but because this project has:
- More than one inbound protocol and more than one outbound technology.
- A translation engine that changes frequently—the most volatile external dependency.
- Business logic that needs to be testable without infrastructure dependencies.
What it solves: business logic should know about technology, but should not depend on it.
What it doesn't solve: deployment isolation, automatic input validation, or security hardening. These are separate concerns that must be designed independently.
The most useful thought to take away from this is not about Hexagonal Architecture specifically—it's about what architecture is for. Architecture patterns serve change management. The question to ask of any pattern is not "is this pattern good" but "what does changing a requirement cost in this structure—how many files must I read, how many tests must I run, what risks am I taking."
Hexagonal Architecture has advantages here and costs here. Knowing both clearly matters more than picking the right label.
Project source:
YuBing-link
/
noveltrans
AI-powered SaaS translation platform for web novels — batch translate with RAG memory, multi-agent collaboration, Stripe billing, Chrome extension & REST API. Java 21 + React 19 + Python.
NovelTrans
A production SaaS backend for AI-powered novel/document translation — multi-engine orchestration, RAG translation memory, Stripe subscription management, team collaboration, and multi-tenant data isolation.
Overview
NovelTrans is a full-stack translation platform built for web novel authors and translators. It replaces the traditional "copy-paste into Google Translate" workflow with an intelligent pipeline that understands context, preserves character name consistency, and learns from past translations — all while reducing LLM API costs through RAG-based semantic reuse.
Three client channels share the same backend:
- React web dashboard — DeepL-style interface with real-time chapter preview
- Chrome extension (MV3) — three modes: full-page, reader mode, text selection
- External REST API — API-key authenticated, for third-party integrations
Features
- Multi-engine AI orchestration — Routes translation requests across LLM (Python FastAPI + OpenAI SDK) and local engines (MTranServer) with probability-based load balancing using rolling 60-second performance windows; MTranServer serves dual purpose — fast translation mode for instant…
Available for backend architecture and payment system design.
United States
NORTH AMERICA
Related News
What Does "Building in Public" Actually Mean in 2026?
19h ago
The Agentic Headless Backend: What Vibe Coders Still Need After the UI Is Done
19h ago
Why I’m Still Learning to Code Even With AI
21h ago
I gave Claude a persistent memory for $0/month using Cloudflare
1d ago
NYT: 'Meta's Embrace of AI Is Making Its Employees Miserable'
1d ago