Hexagonal Architecture — Ghost Order: Adapter Transaction
Orders paid via Stripe in hexagonal app vanish when @Transactional misses external API.
20+ years shipping large-scale distributed systems. Drawn from code that ran under real load.
- Hexagonal Architecture isolates business logic from infrastructure: Ports (interfaces) define boundaries, Adapters implement them
- Driving (primary) adapters: HTTP controllers, CLI commands, message listeners that initiate calls into the core
- Driven (secondary) adapters: database repositories, REST clients, message producers that the core calls
- Performance impact: abstraction adds <5% overhead in most cases; the real cost is premature abstraction when you don't need it yet
- Production insight: transactions that span multiple adapters (e.g., DB write + external API call) are the #1 cause of partial commits and data corruption
- Biggest mistake: over-abstracting every external dependency before you have a proven need for substitution
Imagine your home has a universal power socket on the wall. Your lamp, phone charger, and toaster all plug into it — the socket doesn't care what's plugged in, and each device doesn't care how electricity is generated. Hexagonal Architecture works the same way: your application's core logic sits in the middle like the house wiring, and everything that talks to it — databases, HTTP APIs, message queues — plugs in through standard sockets called Ports, using Adapters shaped to fit each device. Swap your PostgreSQL database for MongoDB? Just swap the adapter. The core never changes.
Every codebase eventually hits the same wall: the business logic is so tangled up with database calls, HTTP frameworks, and third-party SDKs that changing one thing breaks three others. A sprint that should take a day takes a week because your OrderService knows about Hibernate annotations, Spring's @Autowired, and Stripe's API all at once. This isn't a skill problem — it's an architecture problem, and it's exactly the pain that Alistair Cockburn designed Hexagonal Architecture to eliminate in 2005.
The core idea is radical in its simplicity: your application should have no knowledge of the outside world. It shouldn't know whether it's being called by an HTTP request, a CLI command, or a test suite. It shouldn't know whether it's persisting data to PostgreSQL, a flat file, or an in-memory map. All that external detail is pushed behind interfaces — called Ports — and concrete implementations of those interfaces — called Adapters — live entirely outside the application's core domain. The result is a domain model that's pure business logic, nothing else.
By the end of this article you'll understand how to decompose a real feature (an e-commerce order placement flow) into a proper Hexagonal Architecture using Java, why the distinction between Driving (Primary) and Driven (Secondary) adapters matters for testing, how to handle production gotchas like transaction boundaries that cut across adapter layers, and what the honest performance trade-offs are. You'll be able to apply this pattern from day one on a new service and refactor an existing one without a full rewrite.
Why Hexagonal Architecture Is About Boundaries, Not Layers
Hexagonal architecture, also known as ports and adapters, decouples the core business logic from external concerns — databases, APIs, UIs, message queues — by defining explicit ports (interfaces) inside the core and adapters outside that implement them. The core never imports an external library or framework; it only depends on its own ports. This inverts the traditional layered architecture: instead of the core calling the database, the database adapter implements a port the core defines.
In practice, each external system gets its own adapter — one for Postgres, one for a REST API, one for an in-memory store — all conforming to the same port. You can swap adapters without touching core code. The core remains testable in isolation: mock the port, verify behavior. No Spring context, no database connection, no HTTP server needed for unit tests. The hexagonal shape is a visual metaphor: the core is the hexagon, each side is a port, and adapters plug into those sides.
Use hexagonal architecture when your system must survive changes in infrastructure — swapping databases, adding new API consumers, or migrating from monolith to microservices. It shines in domains where business rules are complex and long-lived, and where external integrations are volatile. The cost is indirection: you write an interface and an adapter for every external interaction. The payoff is that a decade later, when you migrate from Oracle to CockroachDB, you change one adapter, not the entire codebase.
Core Concept: Ports Are Contracts, Not Implementation Hints
A Port is a Java interface that lives in the core domain package, typically under io.thecodeforge.core.port. It defines what the core needs from the outside world (driven port) or what the outside world can ask the core to do (driving port). The key rule is: the port interface must be expressed in the language of the domain, not in the language of infrastructure.
For example, a SaveOrderPort driven interface should have a method like OrderId save(Order order) rather than void insertIntoOrderTable(OrderRow row). The core shouldn't know about tables, SQL, or even that the data is persisted. This pure abstraction is what makes swapping adapters trivial.
- The socket shape (interface) never changes — but you can plug different devices in.
- Your house wiring (core logic) doesn't care if you plug in a toaster or a phone charger.
- Each adapter is a plug shaped for one specific socket — but the plug's back end connects to a different external system.
- Don't put universal sockets everywhere — only where you genuinely need to swap.
Driving vs Driven Adapters — The Primary/Secondary Split
Adapters come in two flavours, and the distinction matters for testing, packaging, and debugging.
Driving (Primary) Adapters: Initiate calls into the core. They are the entry points — HTTP controllers, CLI commands, queue listeners, scheduled tasks. They translate external messages into domain commands. In testing, you replace them with your test code (or mock the HTTP layer).
Driven (Secondary) Adapters: Called by the core to perform side effects. They are the exit points — database repositories, REST client proxies, message producers. The core doesn't know about them directly; it only knows the port interface. In testing, you mock or stub the port, never the adapter.
The rule of thumb: driving adapters push calls into the core; driven adapters are pulled by the core.
io.thecodeforge.core.port.driven) and the adapter in the infrastructure package (io.thecodeforge.adapter.driven). This enforces the dependency rule: core never imports from adapter.Dependency Inversion in Practice — The Real Power of Hexagonal
Hexagonal Architecture is a concrete application of the Dependency Inversion Principle (DIP): high-level modules should not depend on low-level modules; both should depend on abstractions. In Hexagonal terms, the core domain defines the abstractions (ports), and adapter modules implement them. The core has no compile-time dependency on any adapter. This means you can develop, test, and deploy the core independently of infrastructure.
To achieve this in Java, use constructor injection. The core's service classes depend only on port interfaces. A configuration class or a dependency injection container (like Spring) wires the adapters at runtime. This wiring layer is itself an adapter — often called the 'Application Configuration' or 'System Assembly' adapter.
Testing Hexagonal Systems — Where the Pattern Shines
One of the biggest wins of Hexagonal Architecture is testability. Because the core depends only on interfaces, you can unit-test all business logic with mocks or in-memory adapters — no database, no network, no startup time. For integration tests, you test each adapter independently against a real resource, using testcontainers or in-memory variants.
- Core unit tests: pure JUnit + mocks for every port. These run in milliseconds and cover all business rules.
- Adapter integration tests: test the adapter against a real instance of the external system (e.g., testcontainers for PostgreSQL, wiremock for REST APIs). These verify serialization, error handling, and timeouts.
- Sliced system tests: compose a real core with real adapters (or a mix) to test the wiring. Don't test every adapter combination — just the critical paths.
- Unit tests (70%): core logic with mocked ports — fast, reliable, high coverage.
- Integration tests (20%): adapter tests against real infrastructure — slow but necessary.
- E2E tests (10%): full wiring test for the most critical user journeys.
- Never test the same business rule in both unit and integration tests — wasted effort.
Production Gotchas: Transactions, Logging, and Circuit Breakers
Real production use reveals several pitfalls that are easy to miss in tutorials. Three of the most common:
- Transaction boundaries crossing adapters: As shown in the production incident, a
@Transactionalin the service only covers the database adapter. If a driven adapter calls an external API, the API call is outside the transaction. If the API fails after the DB commit, you have an inconsistent state. Use the Outbox pattern or Saga pattern. - Logging in the core: Logging is a cross-cutting concern. Don't make the core depend on SLF4J or Log4j directly. Use a port like
LogPortwith an adapter that logs. But most teams accept SLF4J as a standard interface and include it in the core — it's a pragmatic trade-off. - Circuit breakers in adapters: A driven adapter that calls an external API should implement retry and circuit-breaking. Do this inside the adapter, not the core. The core doesn't know about retries; it just calls the port and expects success or gets an exception.
@Transactional annotation on a core service class also covers calls to driven adapters that use different resources (HTTP, messaging). Use @TransactionalEventListener(phase = AFTER_COMMIT) to defer operations or use the Outbox pattern.OrderSaveFailedException). Core catches it and decides retry or failure.Real-World Hexagonal: What Shopify and Netflix Actually Do
Theory is cheap. Let's talk about where this pattern survives production traffic.
Shopify doesn't have a "hexagonal architecture." They have a core domain that processes orders, and that domain talks to payment gateways through contracts. When they add a new payment provider, they write an adapter. The core logic never changes. This isn't about being clever — it's about not rewriting your checkout flow every time Stripe changes their API.
Netflix does the same thing with content delivery. Their recommendation engine doesn't know it's talking to a CDN. It knows it needs to fetch a video stream. Whether that stream comes from Akamai, AWS, or their own Open Connect appliance is irrelevant to the business logic.
The pattern succeeds because it solves a concrete operational problem: external dependencies change. Databases get migrated. APIs get deprecated. If your business logic is tangled with your infrastructure, every external change becomes a rewrite. If it's behind ports, it's a ticket.
This isn't academic. It's survival.
The Four Components That Actually Matter (Skip the Yoga Pants Diagrams)
Every blog post draws a hexagon with labels. I'm going to tell you what each piece actually does in production.
Entities — Your business objects with real behavior. Not anemic structs. An Order should know if it can be shipped. A Subscription should validate its own billing cycle. If your entities are just data bags with getters, you've built an ORM wrapper, not a domain.
Ports — Interfaces that define what the domain needs. Not what tech stack can provide. A UserRepository port has methods like and find_by_email(). It does NOT have save() or query_raw_sql(). The port is the contract. The adapter is the implementation.execute_stored_procedure()
Application Services — These orchestrate. They don't contain business rules. A CreateOrderService fetches the user, validates inventory, creates the order, and dispatches events. The business logic lives in the entities, not the services. If your services have if statements about pricing, you've smuggled domain logic into the wrong layer.
Adapters — The ugly infrastructure glue. They translate between your pristine domain and the grimy real world of SQL, HTTP, and message queues. Adapters should be thin. If your adapter has more logic than the entity it serves, you're leaking abstraction.
These four components. That's the architecture. Everything else is ceremony.
infra, db, or external packages. If it does, that import is a leak. The domain package should only import the standard library and abstractions.Why Hexagonal Pays Off: Speed, Safety, and Swap Costs
Hexagonal architecture delivers three measurable benefits. First, test speed. When business logic depends on interfaces instead of databases or HTTP clients, you substitute mocks or in-memory adapters in unit tests — no spinning up Postgres, no network calls. Second, swap cost. The same port (contract) can have a PostgreSQL adapter today and a DynamoDB adapter tomorrow without touching domain code. Changing payment providers? Write one adapter, plug it in. Third, isolation. Adapter failures (timeouts, rate limits) don't cascade into core logic because ports define the contract, and the adapter handles retries. Teams adopting hexagonal report 40-60% faster test suites for domain modules and lower regression risk when swapping infrastructure. The pattern forces you to put boundaries where they belong: at the system edge, not between your business rules.
Start with the Port, Not the Framework
Getting started with hexagonal architecture means ignoring the database, web framework, and external APIs — resist that instinct. Step one: write the domain interface (port) that your business logic needs. If you're building an order service, define an OrderRepository port with methods like save(order) and findById(id). Step two: implement a fake adapter in-memory — a dict-backed stub. Step three: write your domain logic against the port, passing the adapter. Verify the logic works with a unit test. Only then write the real adapter (Postgres, Redis). This sequence forces you to think about what the domain actually requires, not what the ORM provides. Common pitfall: starting with Django models or Spring repositories leads to leaky abstractions. The port defines the contract; the adapter is just a driver. Start with a test, a port, and a fake.
Overview
Hexagonal Architecture, also called Ports and Adapters, was introduced by Alistair Cockburn in 2005 to solve a fundamental problem: how do we build software that remains decoupled from infrastructure? Traditional layered architectures often leak framework or database concerns into business logic, creating tight coupling that makes testing painful and replacement expensive. The hexagonal pattern inverts this by placing the business domain at the center and expressing all external interactions—databases, APIs, message queues, UIs—through abstract ports. Adapters on the peripheral implement those ports, translating between the outside world and the core. The name comes from the visual metaphor of a hexagon, though the number of sides is arbitrary; the key insight is that the system has multiple, symmetric entry points, not a top-heavy layer stack. This overview sets the stage: hexagonal is about protecting your business rules from change, not about drawing pretty shapes. Every port is a boundary where ownership flips—you control the contract, the adapter handles the how.
Principles
Three principles govern hexagonal architecture. First, Dependency Rule: dependencies point inward. The domain core never imports frameworks, databases, or UI libraries—only plain language abstractions. Second, Port Boundary: a port is a contract defined by the core, not the infrastructure. It specifies what the core needs (e.g., "save this order"), not how the adapter will implement it. Third, Adapter Symmetry: each external actor gets its own adapter. A MySQL adapter implements the OrderRepository port; an HTTP adapter implements the Notifier port. Adapters can be hot-swapped without altering core logic. These principles enable testability—mock the port, test the domain—and change tolerance. When Stripe replaces PayPal, you swap one PaymentPort adapter for another; the approval logic in the core never blinks. The real art: resist the temptation to let the ORM's query patterns dictate your port signatures. Own the contract, own your business rules.
The Ghost Order: When a Transaction Spans Two Adapters
@Transactional annotation on the service method covered both the database write and the Stripe API call.OrderRepository.save() (database) → PaymentAdapter.charge() (Stripe API). The @Transactional only wrapped the database calls; the external API call was outside the transaction. When Stripe succeeded but the database commit failed (e.g., due to a constraint violation), the payment was already captured. No compensation was implemented.- Never assume a transaction boundary covers I/O outside the database; transactions in Java only scope JDBC/JPA interactions.
- Any side-effectful call to a driven adapter (external API, message queue) must be done after the database transaction commits, or be protected by a compensating operation.
- Use the Outbox pattern or Saga pattern for distributed transactions across adapters.
grep -r '@Component\|@Service' src/main/java/io/thecodeforge/adapter/mvn dependency:tree -Dincludes=io.thecodeforgeKey takeaways
Common mistakes to avoid
4 patternsCreating an adapter for every external class
Files API), use it directly in the adapter.Letting framework annotations leak into the core
@Autowired, @Entity, @JsonIgnore annotations that tie them to Spring and Jackson. Swapping frameworks becomes a major refactor.@Entity on a separate JPA entity class, not on the domain object.Not testing adapters with real I/O
Over-abstracting the configuration layer
Interview Questions on This Topic
Explain the difference between Driving (Primary) and Driven (Secondary) adapters. Give an example of each.
OrderService.createOrder(), which at some point calls SaveOrderPort.save() (driven port) implemented by a JPA adapter.Frequently Asked Questions
20+ years shipping large-scale distributed systems. Drawn from code that ran under real load.
That's Architecture. Mark it forged?
10 min read · try the examples if you haven't