Introduction
Hexagonal architecture is nothing new. We can achieve its main principle by using basic language mechanisms and a proper package structure. On top of that, it’s relatively cheap to do that. Especially when we compare it to the cost of the potential technical debt that might appear because we failed to follow good practices when developing our IT project. Still, hexagonal architecture isn’t a very popular technique today. So, let’s start with some sample legacy code and see how we could refactor it to follow the hexagonal architecture principles.
Note: In this example, we use Java and Spring but this technique is language and framework agnostic.
From plain old sample project to Hexagonal Architecture
To make our sample project simpler, let’s assume that we’re developing an application that processes a single resource - messages. We’re exposing one method over HTTP: get all messages. We can start now with the greenfield project that has a well-known structure:
Inside you’ll find some very simple code. Let’s start from the service, our core component:
@Service public class MessageService { private final MessageRepository messageRepository; public MessageService(MessageRepository messageRepository) { this.messageRepository = messageRepository; } public List findAll() { return messageRepository.findAll(); } }
and then repository:
interface MessageRepository extends MongoRepository { }
Dependency Inversion
What’s wrong with that? Our message service uses MessageRepository directly, so it depends on it. It breaks the last SOLID principle - Dependency Inversion. How can we get rid of this dependency? Here’s the answer: we can invert it. Let’s define the interface in a core (service) package.
public interface MessageRepository { List findAll(); }
We can then implement it in the persistence package using files, MongoDB, or any other storage. Now the persistence - the low-level technical component - depends on a high-level abstraction.
Driven port
What did we just do from the Hexagonal Architecture perspective? We have created a secondary (driven) port - MessageRepository - and a secondary adapter - for instance, MongoMessageRepository. Now persistence depends on the domain, not the other way round. It’s worth mentioning here that MessageRepository acts as Service Provider Interface (SPI).
Driver port
We already saw the secondary (driven) port and adapter. What about the primary one? The primary (driver) port serves as the definition of API domain exposed by the domain to the external world. In our case, it’s the MessageService. Perhaps MessageFacade sounds better? But the name isn’t that important - rather, it’s the fact that this is the only entry point to the domain. The API is called by the primary adapter - in our sample, the MessageController. To summarize:
The primary port is API and it’s called by the primary adapter.
The secondary port is SPI and it’s implemented by the secondary adapter.
What else could we improve?
Package structure
Packages are a powerful feature of Java. The package structure should tell us something about the project. What we can say based on the package structure from picture 1 at a first glance? No more than the fact that we use layered architecture. Let’s take a look at how can we improve it to follow the hexagonal architecture approach:
Now the first meaningful part of the package (skipping the company’s domain and project name) contains information about the main business concept - the message service. At this stage, it’s not important if we have controllers and persistency; these are technical details. Digging deeper, we see two main packages - the domain containing entire logic and adapters; the external components dependent on the domain.
Hexagonal Architecture in a real project
The example showed previously is a bit verbose. If you know which component plays which role, the package structure can be simplified and still provide the separation required by hexagonal architecture.
Summary
Keeping the core domain separated is crucial, especially for larger projects. If you’re working on a simple CRUD, perhaps the layered architecture is all you need. On the other hand, the goal of providing hexagonal architecture forces developers to think about the boundaries in the application. And that’s always good. Even if the application they’re working on doesn’t require sophisticated architecture, keeping these concepts in mind is always beneficial in the long-term perspective.