Hexagonal Architecture
Look at the auth-service of Hands-on 3 from the outside. It is called by the gateway over HTTP, and could soon be called by a Kafka consumer or a CLI tool for administrators; your tests want to call it too. It calls other things: account-service via Feign, a JWT library, maybe Redis for revoked tokens. Layer diagrams draw HTTP "on top" and the database "at the bottom", but for the business logic they are the same kind of thing β the outside world β and each of them should be replaceable without touching the login rules.
Hexagonal Architecture β also called Ports & Adapters β was introduced by Alistair Cockburn in 2005. The idea is to place the application core at the centre and expose it through ports (interfaces), which external adapters connect to. The hexagon shape is arbitrary; it signals that the application has multiple equivalent entry/exit points with no privileged side β there is no "top" and "bottom", only inside and outside1.
flowchart LR
subgraph hexagon [Application Core]
domain[Domain Model]
app[Application Services]
inport[Β«portΒ»\nDriving Interfaces]:::port
outport[Β«portΒ»\nDriven Interfaces]:::port
domain <--> app
app --> inport
app --> outport
end
rest([REST Controller]):::adapter -->|driving adapter| inport
cli([CLI]):::adapter -->|driving adapter| inport
test([Test Suite]):::adapter -->|driving adapter| inport
outport -->|driven adapter| db[(Database)]:::adapter
outport -->|driven adapter| mq([Message Broker]):::adapter
outport -->|driven adapter| ext([External API]):::adapter
classDef port fill:#adf,stroke:#36c
classDef adapter fill:#fd9,stroke:#a60 Ports
A port is a pure interface β a contract defined by the application core, not by the external technology. There are two kinds:
| Type | Direction | Defined by | Implemented by | Example |
|---|---|---|---|---|
| Primary / Driving port | Outside β App | The application | The application itself | AuthService interface called by the REST controller |
| Secondary / Driven port | App β Outside | The application | An external adapter | AccountRepository implemented by JPA adapter |
Ownership matters
Both port types are owned and defined by the application core. The adapter is the piece that lives outside and plugs into the port β the core never imports the adapter.
Primary (driving) ports
Primary ports represent what the application can do β they are the use-case API. Any external actor that wants to interact with the application must go through a primary port.
// Defined inside the application core
public interface AuthService {
TokenOut login(String email, String password);
void logout(String token);
}
The implementation of this interface also lives inside the core β it is the application service:
// Also inside the core β depends only on secondary ports
public class AuthServiceImpl implements AuthService {
private final AccountRepository accounts; // β secondary port
private final JwtService jwt;
@Override
public TokenOut login(String email, String password) {
Account account = accounts.findByEmail(email)
.filter(a -> a.matchesPassword(password))
.orElseThrow(InvalidCredentialsException::new);
return new TokenOut(jwt.generate(account));
}
}
Secondary (driven) ports
Secondary ports represent what the application needs from the outside world β persistence, messaging, external APIs. The core defines the interface; the infrastructure provides the implementation.
// Defined inside the application core
public interface AccountRepository {
Optional<Account> findByEmail(String email);
Account save(Account account);
}
Adapters
An adapter is a class that translates between an external technology and a port contract. It lives outside the core and depends on it β never the reverse.
// Adapter: translates HTTP β primary port
@RestController
public class AuthController {
private final AuthService authService; // β primary port
public AuthController(AuthService authService) {
this.authService = authService;
}
@PostMapping("/auth/login")
public ResponseEntity<TokenOut> login(@RequestBody LoginIn in) {
TokenOut token = authService.login(in.email(), in.password());
return ResponseEntity.ok(token);
}
}
// Adapter: translates secondary port β JPA
@Repository
public class AccountJpaAdapter implements AccountRepository {
private final AccountJpaRepository jpa;
@Override
public Optional<Account> findByEmail(String email) {
return jpa.findByEmail(email).map(AccountMapper::toDomain);
}
@Override
public Account save(Account account) {
AccountTable table = AccountMapper.toTable(account);
return AccountMapper.toDomain(jpa.save(table));
}
}
// Swap the JPA adapter for a map in tests β no Spring context needed
public class InMemoryAccountRepository implements AccountRepository {
private final Map<String, Account> store = new HashMap<>();
@Override
public Optional<Account> findByEmail(String email) {
return Optional.ofNullable(store.get(email));
}
@Override
public Account save(Account account) {
store.put(account.email(), account);
return account;
}
}
A request through the hexagon
A login crosses the hexagon twice: in through a primary port, out through a secondary port, and back.
sequenceDiagram
autonumber
participant GW as gateway
participant In as AuthController (driving adapter)
participant Core as AuthServiceImpl (core)
participant Out as AccountJpaAdapter (driven adapter)
participant DB as Postgres
GW->>In: POST /auth/login {email, password}
In->>Core: login(email, password) via AuthService port
Core->>Out: findByEmail(email) via AccountRepository port
Out->>DB: SELECT ... WHERE email = ?
DB-->>Out: row
Out-->>Core: Account (mapped from AccountTable)
Core->>Core: matchesPassword, generate JWT
Core-->>In: TokenOut
In-->>GW: 200 OK + token Replace the first participant with a test and the fourth with InMemoryAccountRepository, and the core runs exactly the same code β that is the whole point.
The store's services as hexagons
| Outside actor | Kind | Adapter in the store |
|---|---|---|
Gateway calling POST /auth/login | driving | AuthResource / AuthController (REST) |
Kafka topic accounts (see Hands-on 9) | driving | a @KafkaListener consumer |
| JUnit test | driving | the test class itself |
account-service | driven | Feign client (AccountController in Hands-on 3) |
| Postgres | driven | AccountJpaAdapter |
| Redis (cache, revoked tokens) | driven | a Redis adapter (see Hands-on 8) |
Kafka (publishing AccountCreated) | driven | a KafkaTemplate adapter |
Hands-on 3, read through this lens
In Hands-on 3, AuthService calls the Feign interface AccountController directly. That interface is defined by the account API module, not by the auth core β so the port is shaped by the remote service, and a change in account's API reaches the login logic. The strict hexagonal version declares a small port in the auth core (for example AccountLookup.findByEmail) and makes the Feign client one adapter behind it.
Full working example
The files below show the complete authentication slice of the auth-service. Each file is annotated with its location relative to the hexagon boundary.
Testing strategy
The key testing insight of Hexagonal Architecture is that the application core can be fully tested without any infrastructure. Replace every driven adapter with an in-memory stub; drive the primary port directly from a test.
flowchart LR
test([JUnit Test]):::adapter -->|drives| AuthService
AuthService --> AccountRepository
AccountRepository -->|implemented by| InMemory([InMemoryAccountRepository]):::adapter
classDef adapter fill:#fd9,stroke:#a60 No Spring @SpringBootTest, no H2, no port 8080 β the test runs in milliseconds and covers the business rule directly.
Suggested package layout
com.example.auth/
βββ domain/ β Domain Model
β βββ Account.java # Aggregate root β no framework annotations
βββ application/ β Application Services + Ports
β βββ AuthService.java # Primary port (interface)
β βββ AuthServiceImpl.java # Primary port implementation
β βββ port/
β βββ AccountRepository.java # Secondary port (interface)
βββ adapter/
β βββ in/
β β βββ AuthController.java # Driving adapter (REST)
β βββ out/
β βββ AccountJpaAdapter.java # Driven adapter (JPA)
β βββ AccountTable.java # @Entity β stays in the adapter
β βββ AccountMapper.java
Common mistakes
Ports defined by the adapter
If the AccountRepository interface mirrors the Spring Data JpaRepository method names, it is being shaped by JPA rather than by the application's needs. Define ports in terms of domain concepts, not persistence concepts.
The core importing the adapter
Any import com.example.adapter.* inside the domain or application package is an immediate violation. Dependency injection (Spring, CDI) should resolve the adapter at runtime, not at compile time.
Wrong port granularity
One port per method (FindAccountByEmailPort, SaveAccountPort, DeleteAccountPortβ¦) leads to interface explosion. Group operations by cohesion β AccountRepository is one port for one concept. On the driving side, one interface per use case (RegisterAccountUseCase, LoginUseCase) is a legitimate style that keeps each interface small (ISP); a single AuthService with a few related methods is also fine. Pick one convention per service and keep it.
Leaking domain objects through the driving adapter
Returning the domain Account (with its passwordHash) straight from the REST controller couples the HTTP contract to the domain β and here leaks a secret. Driving adapters convert domain results into response records (TokenOut, AccountOut).
Key takeaways
- The application has an inside (domain + application services) and an outside (everything else); there is no privileged top or bottom.
- Ports are interfaces owned by the core: driving/primary ports say what the application offers; driven/secondary ports say what it needs.
- Adapters translate between a technology and a port: REST, CLI, Kafka listeners and tests drive the core; JPA, Feign, Redis and Kafka producers are driven by it.
- Ports are named in domain terms, never shaped by the technology behind them.
- Swapping driven adapters for in-memory ones lets the core be tested with plain JUnit in milliseconds β compare with the layers of Clean Architecture, which describe the inside of the hexagon.
Video introductions: CodeOpinion2 (English) and Fernanda Kipper3 (Portuguese).
-
COCKBURN, A. Hexagonal Architecture, 2005.Β β©
-
Hexagonal Architecture β What Is It? Why Should You Use It? by CodeOpinionΒ β©
-
Arquitetura Hexagonal na PrΓ‘tica | Arquitetura com Java e Spring Boot by Fernanda KipperΒ β©
