Hexagonal Architecture in Spring Boot Microservices


As microservices scale in complexity and responsibility, their internal architecture becomes crucial to long-term maintainability and testability. One architecture that helps maintain this modularity is Hexagonal Architecture, also known as Ports and Adapters. Originated by Alistair Cockburn, it promotes a clear separation between the core domain logic and the external concerns like databases, REST APIs, messaging, etc.
What Is Hexagonal Architecture?
Hexagonal Architecture encourages the idea that the application core (business logic) should not depend on anything external. Instead, it defines Ports (interfaces) that represent input/output operations, and Adapters that implement these ports for specific technologies (REST, Kafka, JPA, etc.).
This leads to:
Better testability (the core logic can be tested in isolation).
Enhanced modularity and separation of concerns.
Flexibility in swapping technology implementations without modifying business logic.
Key Components
Application Core: Contains domain models and use cases (services).
Ports: Interfaces that define contracts for inputs (driving ports) and outputs (driven ports).
Adapters: Implement these interfaces, e.g., a REST controller or JPA repository.
Sample Use Case: Banking Microservice for Account Transfer
Let’s build a minimal example using this architecture. But before that lets take few minutes halt at this line and think how would you do that, not a detailed one, just an outline so that you can compare it with the rest of the write-up.
- Domain Layer (Core)
// domain/model/Account.java
public class Account {
private String id;
private BigDecimal balance;
public void transfer(Account target, BigDecimal amount) {
if (this.balance.compareTo(amount) < 0) {
throw new IllegalStateException("Insufficient funds");
}
this.balance = this.balance.subtract(amount);
target.balance = target.balance.add(amount);
}
}
// domain/ports/out/AccountRepository.java
public interface AccountRepository {
Optional<Account> findById(String id);
void save(Account account);
}
// domain/ports/in/TransferService.java
public interface TransferService {
void transfer(String fromId, String toId, BigDecimal amount);
}
// application/TransferServiceImpl.java
public class TransferServiceImpl implements TransferService {
private final AccountRepository accountRepository;
public TransferServiceImpl(AccountRepository accountRepository) {
this.accountRepository = accountRepository;
}
public void transfer(String fromId, String toId, BigDecimal amount) {
Account from = accountRepository.findById(fromId).orElseThrow();
Account to = accountRepository.findById(toId).orElseThrow();
from.transfer(to, amount);
accountRepository.save(from);
accountRepository.save(to);
}
}
2. Adapters Layer
REST Controller (Driving Adapter)
// adapters/in/web/TransferController.java
@RestController
@RequestMapping("/api/transfer")
public class TransferController {
private final TransferService transferService;
public TransferController(TransferService transferService) {
this.transferService = transferService;
}
@PostMapping
public ResponseEntity<Void> transfer(@RequestBody TransferRequest request) {
transferService.transfer(request.getFromId(), request.getToId(), request.getAmount());
return ResponseEntity.ok().build();
}
}
calls core business logic
JPA Repository (Driven Adapter)
// adapters/out/persistence/AccountJpaEntity.java
@Entity
public class AccountJpaEntity {
@Id private String id;
private BigDecimal balance;
// getters/setters
}
// adapters/out/persistence/AccountJpaRepository.java
public interface AccountJpaRepository extends JpaRepository<AccountJpaEntity, String> {}
// adapters/out/persistence/AccountRepositoryImpl.java
@Component
public class AccountRepositoryImpl implements AccountRepository {
private final AccountJpaRepository jpaRepo;
public AccountRepositoryImpl(AccountJpaRepository jpaRepo) {
this.jpaRepo = jpaRepo;
}
public Optional<Account> findById(String id) {
return jpaRepo.findById(id)
.map(e -> new Account(e.getId(), e.getBalance()));
}
public void save(Account account) {
AccountJpaEntity entity = new AccountJpaEntity(account.getId(), account.getBalance());
jpaRepo.save(entity);
}
}
Business logic invokes this adapter
3. Spring Configuration
// config/ServiceConfig.java
@Configuration
public class ServiceConfig {
@Bean
public TransferService transferService(AccountRepository accountRepository) {
return new TransferServiceImpl(accountRepository);
}
}
Differences
On a high level there is not much difference from a typical crud operation however instead of controller calling service calling dao calling repositories, we have decoupled the core business logic to self-sustained entity and have provided interfaces around it in form of TransferService
and AccountRepository
which are the way to integrate through business logic. Now underlying business logic can change without affecting the adapters and vice-versa.
Benefits of Hexagonal Architecture
Core logic is decoupled from framework-specific code.
Easy to test the application by mocking ports.
Flexible to swap technologies (e.g., move from JPA to MongoDB).
Improves readability by making dependencies explicit.
When to Use
In complex domains where business rules must be protected.
In microservices with external dependencies (databases, APIs, message queues).
When designing for long-term maintainability.
Why does Hexagonal Architecture makes sense
The name “Hexagonal Architecture” comes from how Alistair Cockburn originally drew the pattern: a six-sided shape (a hexagon) representing the application’s core, with each side offering or consuming a “port.” Around those ports you plug in “adapters” (for the web, the database, messaging systems, external APIs, tests, etc.).
Here’s why the hexagon makes sense:
Symmetry of Ports
- You can attach any number of adapters on any side — HTTP on one edge, a message queue on another, a command-line runner on a third — without your core logic knowing or caring.
No “Top” or “Bottom” Dependencies
- By centering the domain inside a regular polygon, you emphasize that the core has no inherent upstream or downstream — it simply exposes ports and accepts calls.
Visual Clarity
- Six sides are enough to suggest “multiple directions” without overcrowding. The hexagon is a convenient, memorable shape to draw and to think about when mapping your domain’s entry points.
In practice you might draw fewer or more sides, but the hexagon metaphor reminds you to keep your business rules in the center and to isolate all technology-specific code in adapter layers at the edges.
Conclusion
Hexagonal Architecture adds structure and clarity to your Spring Boot applications, enabling you to scale and evolve with confidence. Though it might add initial complexity, the long-term benefits in modularity, testability, and separation of concerns make it a powerful design approach for production-grade microservices.
Next —
Can we extrapolate and apply this hexagonal architecture in microservices in a way that one microservice can hold business core while dependent microservices work as adapters. But that’s for next post some other day.
Originally published on Medium
Subscribe to my newsletter
Read articles from Rahul K directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Rahul K
Rahul K
I write about what makes good software great — beyond the features. Exploring performance, accessibility, reliability, and more.