The Hexagonal Architecture Explained | Ports and Adapters Pattern
In this article, we explore the Hexagonal Architecture, as outlined by Alistair Cockburn in 2005, commonly referred to as the ports and adapters pattern. I will explain from a theoretical standpoint but also offer a concrete example that I’ve shared on GitHub. At the end, we will compare the hexagonal architecture to the clean and onion architectures that we have examined over the past few months.. The same content is available on my YouTube channel.
Hexagonal Architecture Defined
The hexagonal architecture is a software design pattern that separates the core business logic of the application from all external actors, like UI and DB. There is no direct communication between external actors and the business logic. The business logic is placed within the inner hexagon and isolated from the external actors through ports and adapters.
This strong segregation of the application’s core is motivated by two key goals of the hexagonal architecture:
- Being able to write and test the business logic without any external dependency.
- And reuse the same business logic to support multiple integrations.
But why are these goals important?
Alistair Cockburn formalized the hexagonal architecture in 2005. He had noticed that many applications using the traditional three-layer architecture do a poor job at containing the business logic in the domain layer. Over time the business logic spreads to all layers, making such applications difficult to test and maintain. These applications are also inflexible. If the business logic spreads to the presentation layer, we cannot reuse it to build a new user interface or integrate another application. Similarly, if the business logic spreads to the data layer, we are forced to rewrite it when the infrastructure changes.
Ports and adapters are his solution to the problem.
Ports and Adapters Definition
A port is an interface that defines a protocol supported by the application. By interface we mean that it’s not a concrete class. It has no logic. It’s just a contract between the application and the external world. For this reason, we place it at the edge of the inner hexagon.
An adapter is a concrete class that links a port with an external actor. For each port, we can have multiple adapters. This is how we support multiple external actors with the same core business logic.
We classify ports and adapters as primary or secondary based on the direction of information flow. This is important because it defines whether the port is implemented in the application’s core or outside it.
Primary, driving, or input ports allow an external actor to interact with the application. In this setup the primary adapter invokes the port interface. As a result, the port’s implementation resides in the application’s core. The take-away here is that we can implement the business logic of the primary port without knowing which actor is going to invoke it. We don’t depend on the UI.
Secondary, driven, or output ports are those invoked by the application to communicate with an external actor, typically a database, message queue, or another application. In this case, the port’s implementation is the adapter and lives outside the application’s core. The idea here is that we can write our business logic without depending on the infrastructure.
Summing up:
- All ports are interfaces.
- On the primary side, adapters invoke ports that are implemented in the application core.
- On the secondary side, the application invokes ports that are implemented by adapters.
This is the essence of the ports and adapter pattern. Now before we code a sample application to see it in action, I want to highlight that we can use this pattern in conjunction with many other architecture styles whenever want to isolate a layer in our application from external dependencies. We can also use it at a system level to isolate our system of microservices from clients and third-party systems. Ports and adapters can be used to implement the anti-corruption layer described by Eric Evans in his domain-driven design book.
Another thing… The hexagon has no real meaning. Cockburn just wanted a shape that allowed to model multiple ports and adapters, and that was in clear contrast to the layered architecture. He wanted to stress that the business logic is at the center and not on top of the data layer.
He also didn’t define what the ideal number of ports is. In a simple app, you have at least two ports, one to model inputs and the other outputs. But you could decide to define more ports. For example, you could have one port per use case. You can use as many ports as you need.
Hexagonal Architecture Code Example
Now, how do we code an application using this pattern?
I already coded a sample application to save us time. I’ll just highlight the most important steps. You can follow by cloning or browsing the GitHub repository: https://github.com/marcolenzo/hexagonal-architecture-spring-sample
The app supports only one use-case: withdrawing money from a bank account with the only rules being that the account number must exist, and its balance can never be negative.
In the spirit of the hexagonal architecture, we should start by writing our business logic without external dependencies; ideally, using plain programming language… no frameworks. The steps I like to follow are:
- Creating the application’s core boundary.
- Defining the ports.
- Writing and testing the domain logic.
- Writing and testing the adapters.
- Configuring the application context.
In Java, we can use packages or modules to define application layers. In this case, we define a core package that represents the inner hexagon where we place all the business logic.
With such a simple application, we need only two ports. I modelled the primary port as a use case that raises an exception if the account is unknown or has insufficient funds, which covers our requirements.
We also need to store the data somewhere, so the secondary port is an Account Repository with a method to retrieve the account details by account number, and another one to save or update the account.
Now we can code the business logic. I love domain-driven design, so I validate the account balance right within the Account entity. As a good developer, I wrote tests to make sure the code works as expected.
I implemented the primary port, which is our “Withdraw Funds Use Case” in a service class where I also use the secondary port to validate the account number and update the account balance when the request is successful. Obviously, I wrote tests as well to make sure that everything is in order.
Now I want to highlight that this class is extremely important because it’s proving us that:
- We can code our application without having any dependencies on what invokes the primary port, it could be an HTTP API or CLI.
- We can use the secondary port without any knowledge of the underlying technology, it could be the file system, memory, or any database type.
- We can also use a plain programming language without the aid of any framework.
Now we can write the adapters.
I created two adapters that implement the “Account Repository” to demonstrate the power of the pattern. One allows us to save data in memory. I’m just using a HashMap. Very simple. The other saves data on MongoDB. This is more interesting because you can see that I am using an “Account Document” class. If we look at it, it has the same properties of the original Account class but just a few extra annotations. The reason I did this is to keep the application’s core dependency free. The annotations in the “Account Document” are part of Spring Data framework. This is the price to pay if you want to be purist about this pattern.
The primary adapter is a simple Rest Controller written using Spring MVC. We know this is a primary adapter because it’s invoking the primary port. I’ll leave it up to you to create an additional adapter that implements a CLI.
As for the configuration stage, I included two configurations classes that show how I define different application context profiles. When the local profile is active, we use the in-memory repository; otherwise, we store data in MongoDB.
That’s all!
Clean vs Onion vs Hexagonal Architecture
Now that we have covered the hexagonal, onion, and clean architecture in the last two months, which one should we use in our applications?
The core principle of these three architectures is the same: decoupling the business logic from all other concerns like infrastructure and UI. All architectures place the business logic at the very center of their diagram. The main difference is that the Onion and Clean architectures suggest names for the layers of the application, while the hexagonal does not. On the other hand, the hexagonal offers a practical approach to isolate layers from each other, and in fact Uncle Bob uses it in the clean architecture to show how to decouple the application business logic from controllers and presenters.
That said software architecture is about trade-offs. My suggestion is to experiment with the three approaches and make sure you can isolate layers in your application when necessary. As you saw in the coding section of this video, there’s a price to pay to isolate a layer, which is not worth it if your app is very simple. Things that caused friction 20 years ago are much easier today. For example, running a local database on your machine takes only one command line with Docker.