Hexagonal
Hexagonal Architecture, also called Ports & Adapters, organizes a system so that:
- The core business logic is at the center.
- All interactions with the outside world (UI, database, APIs, devices) go through ports.
- External components connect via adapters.
- The core is independent of frameworks, UI, and infrastructure.
This architecture is called βhexagonalβ because the core can have multiple ports, like the sides of a hexagon, each representing a way external systems interact with it.
Key Conceptsβ
Core Domain (Application + Domain Logic)β
- Contains business rules and use cases.
- Does not depend on UI, databases, frameworks, or external systems.
Portsβ
Ports are interfaces defined by the core that specify:
- What the core expects from the outside world.
- What the outside world can request from the core.
There are two types:
| Port Type | Direction | Purpose |
|---|---|---|
| Inbound Ports | Outside β Core | Expose use cases (e.g., PlaceOrder, RegisterUser) |
| Outbound Ports | Core β Outside | Declare dependencies (e.g., SaveOrder, SendEmail) |
Adaptersβ
- Adapters are implementations of ports:
- They convert external input/output into a form the core understands.
Each external technology (web UI, REST API, database, message queue) gets its own adapter.
| Adapter Type | Role |
|---|---|
| Inbound Adapter | Calls inbound ports (e.g., REST controller, CLI handler) |
| Outbound Adapter | Implements outbound ports (e.g., database repository, email sender) |
Rules of Hexagonal Architectureβ
- The core must not depend on adapters or frameworks.
- All communication happens via ports (interfaces).
- Dependencies point inward toward the core.
- External systems can be replaced without changing core logic.
Example: Online Ordering Systemβ
Letβs design a simple Order Management System using Hexagonal Architecture.
Step 1: Core Domain & Use Case (Inside the Hexagon)β
Inbound Port (Use Case Interface)β
public interface PlaceOrderUseCase {
void placeOrder(int customerId, List<Item> items);
}
Outbound Ports (Dependencies)β
public interface OrderRepository {
void save(Order order);
}
public interface PaymentGateway {
void processPayment(Order order);
}
Domain Entityβ
public class Order {
private int customerId;
private List<Item> items;
public Order(int customerId, List<Item> items) {
this.customerId = customerId;
this.items = items;
}
public double totalAmount() {
return items.stream().mapToDouble(Item::getPrice).sum();
}
}
Use Case Implementation (Application Service)β
public class PlaceOrderService implements PlaceOrderUseCase {
private final OrderRepository orderRepository;
private final PaymentGateway paymentGateway;
public PlaceOrderService(OrderRepository orderRepository, PaymentGateway paymentGateway) {
this.orderRepository = orderRepository;
this.paymentGateway = paymentGateway;
}
@Override
public void placeOrder(int customerId, List<Item> items) {
Order order = new Order(customerId, items);
paymentGateway.processPayment(order);
orderRepository.save(order);
}
}
- Depends only on interfaces.
- Has no knowledge of web frameworks, databases, or payment providers.
Step 2: Inbound Adapter (e.g., Web Controller)β
@RestController
public class OrderController {
private final PlaceOrderUseCase placeOrderUseCase;
public OrderController(PlaceOrderUseCase placeOrderUseCase) {
this.placeOrderUseCase = placeOrderUseCase;
}
@PostMapping("/orders")
public void placeOrder(@RequestBody OrderRequest request) {
placeOrderUseCase.placeOrder(request.getCustomerId(), request.getItems());
}
}
Role: Translates HTTP requests into calls to the core use case.
Step 3: Outbound Adapters (Infrastructure)β
Database Adapterβ
public class JpaOrderRepository implements OrderRepository {
@Override
public void save(Order order) {
// Save order using JPA / SQL / NoSQL
}
}
Payment Adapterβ
public class StripePaymentGateway implements PaymentGateway {
@Override
public void processPayment(Order order) {
// Call Stripe API
}
}
Role: Implements the coreβs required ports using specific technologies.
How the Flow Worksβ
User β Web UI β Inbound Adapter β Inbound Port β Core Logic β Outbound Ports β Outbound Adapters β External Systems
Advantages
- Core logic is framework-independent
- Highly testable (mock ports)
- Easy to swap UI, DB, or external services
- Promotes clean object-oriented design
- Encourages SOLID principles (especially Dependency Inversion)
Disadvantages
- More interfaces β more code
- Higher learning curve
- Overkill for very small/simple projects
Comparison to Layered Architectureβ
F
| Layered Architecture | Hexagonal Architecture |
|---|---|
| Structure by technical layers | Structure by business intent |
| UI β Service β Domain β DB | Core β Ports β Adapters |
| Dependencies flow downward | Dependencies flow inward |
| Frameworks often in core | Core is framework-free |