Hexagonal Architecture in Practice: Ports, Adapters, and Real Use Cases
Structuring Applications for Testability and Longevity
As software systems grow in complexity, maintaining a clean separation of concerns becomes essential—not just for testability but also for adaptability and longevity. One architectural pattern that addresses this challenge elegantly is Hexagonal Architecture, also known as the Ports and Adapters architecture.
In this article, we’ll break down Hexagonal Architecture, explore how it enables loosely coupled design, and walk through real-world use cases to show how it can future-proof your applications.
What Is Hexagonal Architecture?
Coined by Alistair Cockburn, Hexagonal Architecture promotes the idea that the core business logic should be independent of external concerns such as databases, web frameworks, or messaging systems.
The pattern visualizes the system as a hexagon with:
- Inside: The application core (domain and use cases).
- Outside: The infrastructure and interfaces (e.g., UI, DB, APIs).
- Ports: Abstractions that define how the core communicates with the outside world.
- Adapters: Implementations of those ports (e.g., JDBC adapter, REST controller).
This separation allows the core logic to remain untouched when infrastructure details change.
Core Components
1. Ports
- Primary Ports: Define the entry points into your application (e.g., use case interfaces).
- Secondary Ports: Define how your application interacts with external systems (e.g., repository interfaces).
2. Adapters
- Driving Adapters: Drive the application by calling primary ports (e.g., controllers, CLI).
- Driven Adapters: Implement secondary ports to handle infrastructure (e.g., DAOs, API clients).
3. Application Core
This is where your business rules, entities, and use cases live. It has no dependencies on frameworks or infrastructure.
Benefits of Hexagonal Architecture
✅ Testability: Core logic can be tested in isolation without mocking web servers or databases.
✅ Maintainability: Easy to swap out adapters without affecting the core logic.
✅ Scalability: Decouple the app from infrastructure—great for evolving or distributed systems.
✅ Flexibility: Clean separation of concerns leads to better modularity and clearer code boundaries.
✅ Longevity: Adapters may change (e.g., migrating from MySQL to PostgreSQL), but the core remains stable.
A Real-World Example: Bank Account Management System
Let’s walk through a simplified system that manages bank accounts.
Application Core
Domain Model: BankAccount.java
public class BankAccount {
private final String accountId;
private BigDecimal balance;
public void deposit(BigDecimal amount) { ... }
public void withdraw(BigDecimal amount) { ... }
}
Use Case Port: AccountService.java
public interface AccountService {
void deposit(String accountId, BigDecimal amount);
void withdraw(String accountId, BigDecimal amount);
}
Secondary Port: AccountRepository.java
public interface AccountRepository {
Optional<BankAccount> findById(String accountId);
void save(BankAccount account);
}
Adapters
Driving Adapter: AccountController.java
@RestController
public class AccountController {
private final AccountService service;
@PostMapping("/deposit")
public ResponseEntity<?> deposit(@RequestBody DepositRequest request) {
service.deposit(request.getAccountId(), request.getAmount());
return ResponseEntity.ok().build();
}
}
Driven Adapter: JpaAccountRepository.java
@Repository
public class JpaAccountRepository implements AccountRepository {
// Maps domain model to JPA entity and persists
}
Now, if you want to replace your database or switch from REST to gRPC, you only touch the adapters—not the business logic.
Real Use Cases
1. Legacy Modernization
Legacy systems often entangle business logic with infrastructure. By refactoring toward Hexagonal Architecture, teams can extract and protect domain logic before gradually modernizing interfaces (e.g., moving from SOAP to REST or REST to gRPC).
2. Test-Driven Development (TDD)
With clearly defined ports, you can mock dependencies and focus tests on domain logic, leading to faster, more reliable unit testing and CI/CD pipelines.
3. Multiple Interfaces
Want your application to support both REST and CLI? Implement two driving adapters targeting the same ports—no changes to your core logic.
4. Plug-and-Play Adapters
In enterprise applications, business logic often remains stable while integrations evolve (e.g., from in-house databases to cloud-managed services). Adapters make it easy to “plug” new infrastructure with minimal disruption.
Drawbacks and Considerations
🟡 Initial Complexity: For small projects, this separation may feel like overengineering.
🟡 Learning Curve: Developers unfamiliar with the pattern may struggle without training or conventions.
🟡 More Interfaces: You’ll write more interfaces and boilerplate, though this can be reduced with modern frameworks and tools.
Best Practices
- Treat the domain model as sacred—avoid leaking framework-specific annotations into core classes.
- Inversion of control is key—application core should never know about adapters.
- Use dependency injection to wire adapters to ports cleanly.
- Name clearly—use suffixes like
Port,Adapter, andServicefor clarity.
Conclusion
Hexagonal Architecture isn’t just a fancy diagram—it’s a practical, battle-tested approach to building maintainable and resilient software. Whether you’re building a microservice or a monolith, it helps you think in terms of core responsibilities vs. infrastructure concerns.
By enforcing boundaries and favoring abstraction, Hexagonal Architecture helps your applications remain testable, adaptable, and ready for change—long after the frameworks and databases have moved on.



