Hexagonal Architecture (Ports and Adapters) — Clean Boundaries
Fast recall
Quick revision
Core answer
Domain at the center, ports owned by the domain (not by infrastructure), distinction between driving and driven adapters, and how this enables testability without infrastructure.
Defining ports as adapter contracts (e.g., 'JpaUserRepository' with JPA-specific methods) instead of domain contracts. Importing JPA/Spring annotations into the domain.
Compare with Clean Architecture (similar but slightly different layering), discuss why the 'hexagon' shape is symbolic (any number of adapters), and walk through swapping a JPA adapter for an in-memory adapter for tests.
Interview-ready guide
Interview answer
The idea
Alistair Cockburn, 2005. Motivation: application core should be testable without UI, DB, or framework.
In layered Spring Boot, domain often imports @Entity, @Transactional, JPA cascades — framework woven through business code. Replace JPA with Mongo? Big rewrite. Test without Spring? Hard.
Hexagonal flips this: domain defines what it needs through interfaces (ports); adapters on the outside provide implementations. Domain knows nothing about HTTP, JPA, Kafka, or any framework.
'Hexagon' is symbolic — just shows any number of adapters can plug into the same domain. HTTP API, CLI, Kafka consumer all reuse the same use cases.
Architecture
Left: driving adapters (REST controller, Kafka consumer, CLI). They call driving ports (use case interfaces).
Center: application core. Use cases, domain model. Defines driving ports and driven ports.
Right: driven adapters (JPA repository, Redis cache, Stripe gateway). They implement driven ports.
Dependency rule: all arrows point INTO the core. Core never imports adapters.
Ports vs adapters — concrete
Domain has plain Java records (Order, OrderItem). PlaceOrderUseCase is the driving port — just an interface. PlaceOrderService implements it; depends on OrderRepository and InventoryGateway, which are driven ports owned by the core.
OrderController (driving adapter) injects PlaceOrderUseCase, calls it.
JpaOrderRepository (driven adapter) implements OrderRepository, converts between domain Order and JPA entity.
Use case knows nothing of JPA, HTTP, Spring annotations.
Driving vs driven distinction
Driving adapters initiate calls into the app; depend on driving ports. Multiple drivers can share use cases — HTTP + Kafka both call PlaceOrderUseCase.
Driven adapters are called by the app; implement driven ports. Driven port shape is driven by the use case's need, not the technology.
Asymmetry: driving adapters call ports; driven adapters implement ports. This direction — always inward — is what makes the core decoupled.
Testability — the big win
Unit-test PlaceOrderService with in-memory OrderRepository and a fake InventoryGateway. No Spring context, no DB, no Testcontainers. Pure JUnit. Tests run in milliseconds.
Integration tests still exist for adapters — JPA repo against real Postgres — but use case logic tested in pure isolation.
Hexagonal vs Clean Architecture
Same family. Both put domain at center, dependency rule inward, isolate frameworks. Differences mostly stylistic.
Hexagonal: ports + adapters. Lean. 2-3 packages. Clean: four concentric circles, more named roles (entities, use cases, interface adapters, frameworks).
In practice the same idea — most teams build something in between.
Common mistakes
- Defining ports as JPA-shaped — leaks JPA into core
- Importing Spring/JPA annotations into the domain — even @Component on use case is a smell
- Anemic domain entities — hexagonal alone doesn't make domain rich; combine with DDD
- Adapters calling adapters — they should communicate through the core
- DTOs as domain objects — adapters convert DTO ↔ domain at the boundary
Recommendation
Use Hexagonal Architecture when the domain is non-trivial, multiple input adapters need to share business logic, or when framework-free testability is a goal. Define ports in the core's vocabulary, not the adapter's. Combine with DDD for rich domain models. Keep the core free of Spring/JPA/HTTP annotations — wire dependencies in an outer @Configuration class. For simple CRUD services, layered architecture is fine; reach for hexagonal when complexity, multiple adapters, or testability demand it.
Hexagonal Architecture (Ports and Adapters) — Clean Boundaries
Direct Answer
Hexagonal Architecture (Ports and Adapters) puts the domain at the center and isolates it from infrastructure (DB, HTTP, queues, UI) via ports (interfaces defined by the domain) and adapters (implementations on the outside). The dependency rule: outside knows about inside; inside never imports outside. Driving adapters (HTTP, CLI, Kafka consumer) call driving ports (use case interfaces); driven adapters (JPA, Redis, SMS provider) implement driven ports (repository / gateway interfaces). The result: a framework-free, swappable, fully testable domain.
Common Mistakes & Pitfalls
Defining ports as adapter contracts (e.g., 'JpaUserRepository' with JPA-specific methods) instead of domain contracts. Importing JPA/Spring annotations into the domain.
Check your understanding
Answer these without looking back at the article.
- Walk through swapping a JPA adapter for an in-memory adapter in tests — what changes in the core?
- How is Hexagonal different from Clean Architecture?
- Where does validation live in a hexagonal application — controller, use case, or domain?
- How do you handle transactions in a hexagonal architecture without polluting the core with @Transactional?
Related interview prep categories
1Want a guided curriculum order?
These modules are also sequenced in our flagship backend interview track — open it when you prefer an ordered path instead of browsing by topic.