Hexagonal Architecture
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 side1.
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
@Override
public TokenOut login(String email, String password) {
Account account = accounts.findByEmail(email)
.orElseThrow(() -> new InvalidCredentialsException());
account.validatePassword(password);
return TokenOut.issue(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;
}
}
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 class AuthServiceTest {
InMemoryAccountRepository accounts = new InMemoryAccountRepository();
AuthService service = new AuthServiceImpl(accounts);
@Test
void loginSucceeds_whenCredentialsAreValid() {
accounts.save(Account.of("user@example.com", passwordHash("secret")));
TokenOut token = service.login("user@example.com", "secret");
assertThat(token).isNotNull();
}
}
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.
Too many ports
One port per use case leads to interface explosion. Group related operations — AccountRepository is one port, not separate ports for findByEmail, save, and delete.
-
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 ↩
