Hexagonal Architecture
The main idea is relatively simple: business rules should be at the center of the application and shouldn't depend on the technologies around them.
Databases, APIs, Kafka, Redis, web frameworks, and other technologies are important, of course, but from the application's point of view, they are external details.
What is Hexagonal Architecture?
Hexagonal Architecture was proposed by Alistair Cockburn with the idea that an application should be usable and testable independently of the technologies connected to it. In other words, our business rules shouldn't be directly coupled to external technologies.
Let's take a simple example: Imagine an application with a feature for creating users. Today, it receives requests through a REST API and stores users in PostgreSQL.
In the future, however, we might need to create users from Kafka messages, or maybe replace PostgreSQL with MongoDB.
Ideally, none of these changes should require major changes to our business rules. This is exactly the kind of problem Hexagonal Architecture tries to solve.
A simple way to visualize it is:
External technologies connect to our application through well-defined points called Ports, while the components responsible for making those connections are called Adapters.
The Application Core
One of the most important ideas behind Hexagonal Architecture is protecting the core of the application, because that's where the parts that represent the actual behavior of the system live.
We can think of the core as having two main parts: the Domain, which contains business concepts and rules, and the Application, which contains the use cases responsible for coordinating those rules.
Think about a banking system. The rule that an account cannot withdraw more money than its available balance belongs to the domain.
On the other hand, "transfer money from account A to account B" is an application operation that needs to coordinate several steps to make that transfer happen.
That use case might need to:
- Find account A.
- Find account B.
- Perform the debit and credit operations.
- Persist the changes.
- Publish an event.
The Use Case is responsible for coordinating this flow, while the important business rules remain in the domain.
The Direction of Dependencies
Dependencies should point toward the core, and the core shouldn't know about external details.
For example, this would create unwanted coupling:
public class CreateUserService {
private final JpaRepository<UserEntity, Long> repository;
}
Our application logic now knows directly about JPA, which means an infrastructure decision has leaked into the application core.
With Hexagonal Architecture, we want to invert that dependency.
Instead of having the core depend on JPA, the core defines what it needs:
public interface UserRepository {
User save(User user);
}
This interface knows nothing about JPA, Hibernate, PostgreSQL, MongoDB, or Spring Data. It simply says:
"For my application to work, I need something capable of saving a user."
The infrastructure is responsible for deciding how that will actually happen.
This idea is closely related to the Dependency Inversion Principle, the "D" in SOLID.
Ports
Ports are the communication points between the application core and the outside world. They are commonly divided into Input Ports and Output Ports.
Input Ports
An Input Port represents an operation that the application exposes to the outside world. In Java, it can be represented by an interface that defines the contract of a use case.
For example:
public interface CreateUserUseCase {
User execute(CreateUserCommand command);
}
This represents something our application knows how to do: create a user.
A REST Controller can call this port, but so can a CLI or a Kafka Consumer. None of them need to know the internal details of how the use case is implemented.
For example:
@RestController
@RequestMapping("/users")
public class UserController {
private final CreateUserUseCase createUserUseCase;
public UserController(CreateUserUseCase createUserUseCase) {
this.createUserUseCase = createUserUseCase;
}
@PostMapping
public ResponseEntity<UserResponse> create(
@RequestBody CreateUserRequest request
) {
var command = new CreateUserCommand(
request.name(),
request.email()
);
var user = createUserUseCase.execute(command);
return ResponseEntity.ok(
new UserResponse(
user.getId(),
user.getName(),
user.getEmail()
)
);
}
}
Output Ports
Now imagine that our CreateUserUseCase needs to save a user.
To do that, the core needs to communicate with something external — in this case, a database.
But there's an important detail here: the Use Case doesn't need to know which database is being used.
It only needs to express a requirement:
"I need to save a user."
We can represent that requirement with an Output Port:
public interface UserRepository {
User save(User user);
boolean existsByEmail(String email);
}
Now we can implement our use case:
public class CreateUserService implements CreateUserUseCase {
private final UserRepository userRepository;
public CreateUserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Override
public User execute(CreateUserCommand command) {
if (userRepository.existsByEmail(command.email())) {
throw new EmailAlreadyExistsException();
}
var user = User.create(
command.name(),
command.email()
);
return userRepository.save(user);
}
}
Notice that CreateUserService knows about UserRepository, but it knows nothing about JpaRepository, Hibernate, PostgreSQL, or MongoDB.
From the use case's point of view, those details don't matter.
That decision belongs to the infrastructure.
Adapters
If Ports define the communication points between the core and the outside world, Adapters are responsible for connecting those points to specific technologies.
They can be divided into two groups: Input Adapters and Output Adapters. An Input Adapter starts an interaction with our application. A REST Controller, Kafka Consumer, or CLI are good examples. An Output Adapter connects our application to something external, such as PostgreSQL, MongoDB, Redis, or a Kafka Producer.
There's an important distinction here:
PostgreSQL itself is not an Adapter. The Adapter is the code responsible for connecting our application to PostgreSQL.
For example:
@Component
public class UserRepositoryAdapter implements UserRepository {
private final SpringDataUserRepository repository;
public UserRepositoryAdapter(
SpringDataUserRepository repository
) {
this.repository = repository;
}
@Override
public User save(User user) {
var entity = UserEntity.fromDomain(user);
var saved = repository.save(entity);
return saved.toDomain();
}
@Override
public boolean existsByEmail(String email) {
return repository.existsByEmail(email);
}
}
If we decide to use MongoDB tomorrow, we could create a MongoUserRepositoryAdapter implementing the same UserRepository interface.
The CreateUserService wouldn't need to change.
Where Does the Domain Fit In?
So far, we've talked a lot about the "core", but the domain and the application aren't exactly the same thing.
Understanding this difference helps avoid a common mistake: putting every business rule inside Use Cases.
Imagine we have an Account entity:
public class Account {
private AccountId id;
private Money balance;
public void withdraw(Money amount) {
if (amount.isGreaterThan(balance)) {
throw new InsufficientBalanceException();
}
balance = balance.subtract(amount);
}
}
This validation shouldn't live in a Controller, a database, or a Use Case.
It belongs to the rules of an account itself.
The domain shouldn't need to depend on @RestController, @Entity, @Repository, KafkaTemplate, or RedisTemplate.
Ideally, the domain should be able to exist without even knowing that these technologies exist.
One Core, Different Technologies
One interesting consequence of this decoupling is that we can connect different adapters to the same application core.
Imagine we have a CreateUserUseCase.
Today, it's called by a REST API, but tomorrow we might add a Kafka Consumer.
Both can use exactly the same Use Case.
The same idea applies to the output side:
An in-memory adapter can also be useful for certain types of tests.
This shows one of the original ideas behind the architecture quite well:
"The application should be able to work regardless of how it is triggered or which technologies are connected to it."
When Does It Make Sense?
Hexagonal Architecture tends to make more sense for applications with:
- Relevant business rules.
- Multiple integrations.
- Technologies that may need to be replaced or added over time.
- Different ways of interacting with the application.
- A strong need for isolated tests.
- Systems expected to live and evolve for a long time.
- A domain that needs to evolve independently from infrastructure.
Imagine, for example, a financial application that receives operations through REST, Kafka, or even Batch jobs, while also communicating with PostgreSQL, Redis, Kafka, and external banking APIs.
In this kind of system, controlling dependencies becomes increasingly important.
On the other hand, for a small and simple CRUD application, a more traditional architecture might be more than enough.
There's no reason to add architectural complexity just because a particular architecture is popular.
The cost of the architecture also needs to be considered. Creating ports, adapters, and abstractions for every operation adds indirection to the code.
That complexity should solve a real problem in the project.
Conclusion
Hexagonal Architecture isn't about creating more interfaces or following a specific folder structure.
It's about controlling the direction of dependencies.
For me, a simple way to summarize the idea is:
Your business rules should control the abstractions they need, instead of being controlled by the technologies they use.
In the end, Ports, Adapters, Input, and Output are ways of putting this idea into practice.
The exact structure can vary from project to project. What really matters is keeping clear boundaries between business logic and infrastructure.
A few months ago, I implemented some of these concepts in a URL shortener as a study project. If you're curious, the code is available on GitHub.



Top comments (0)