Stage 3 - Hexagonal Architecture
Hexagonal Architecture, also called Ports and Adapters, treats the application as a core surrounded by replaceable adapters. The core exposes ports: interfaces that describe what it needs or what it offers. Adapters implement those ports for HTTP, databases, message brokers, payment providers, files, or tests.

Why This Matters
Architecture is the part of backend development that decides where code is allowed to live and what it is allowed to know. Without architecture, every feature becomes a direct path from HTTP to database to external service and back. That may work for a small demo, but in a real product the same shortcut creates fragile code. A change in the database leaks into the API. A validation rule appears in three services. A transaction starts in the wrong place. A class becomes impossible to test without starting the whole application.
Good architecture is not about drawing impressive diagrams. It is about reducing the cost of change. A developer should be able to add a field, change a rule, replace an external provider, or test a use case without rewriting unrelated parts of the system. The design should make the common path obvious and the dangerous path difficult.
How To Think About It
Start with responsibility. Ask what decision a piece of code owns. A controller owns HTTP details. A use case owns application flow. A repository owns persistence access. A mapper owns data conversion. A transaction boundary owns atomicity. When those responsibilities are mixed, bugs become harder to locate because one class is doing several jobs for several reasons.
Then look at direction. Dependencies should usually point from outer details toward stable inner rules, or from high-level policy toward replaceable details through interfaces. If a domain rule imports a web controller, the dependency direction is wrong. If a service creates an HTTP client directly inside a method, the dependency is hidden. If a mapper loads lazy relations from the database by accident, the boundary is leaking.
Concrete Example
@Service
public class CreateOrderUseCase {
private final OrderRepository orders;
private final PaymentPort payments;
@Transactional
public OrderResult create(CreateOrderCommand command) {
Order order = Order.create(command.customerId(), command.items());
payments.reserve(order.total());
return OrderResult.from(orders.save(order));
}
}
This example is small, but it shows several architectural choices. The use case receives dependencies instead of constructing them. The payment system is represented by a port, not a concrete SDK class. The transaction boundary surrounds the business operation. The API layer can map a request DTO into CreateOrderCommand before this code runs, and the response DTO can be built after the use case returns.
Useful Reference
| Concept | Meaning |
|---|---|
| Controller | HTTP input and output |
| Use case | Application flow |
| Repository | Persistence access |
| Boundary | Clear ownership line |
Common Mistakes
- Treating folders as architecture while dependencies still point everywhere.
- Adding abstractions before there is a real reason to change an implementation.
- Letting controllers contain business rules because it is faster today.
- Returning entities from API endpoints and calling that simple.
- Placing transaction boundaries around helper methods instead of use cases.
Understanding Checklist
- I can explain which layer or boundary owns the decision in this topic.
- I can identify which dependencies should point inward and which should stay outside.
- I can name one concrete bug this design prevents.
- I can tell when the principle is being overused.
Practice Before the Next Lesson
Take a small order feature and draw where controller, DTO, mapper, use case, repository, external payment adapter, and transaction boundary should live. Then mark one dependency that would be dangerous if it pointed in the opposite direction.