Hexagonal architecture is a great way to build structure to your system and split it into different layers each of which serves a specific purpose.
Do not let the name trick you into thinking that it contains 6 pieces of logic. It is more of a representation of the multiple sides a hexagon has and makes it ideal for apps that have multiple connections with external systems. The hexagon is also a common component to use in UML diagrams.
Now let’s talk about the 3 layers that make Hexagonal architecture.
The way I like to think of adapters is like the I/O of our app. How data reaches into our app and then where does this data go?
That might be a HTTP endpoint that invokes our app, or an EventBridge event that our app is listening to. Then on the opposite end, once the app executes its business logic it has to do something with that data.
A very common scenario is to store that data in a Database like DynamoDB, or MongoDB, or send a notification to the customer. Adapters can be anything that allows our app to have an inbound or outbound communication with the outside world.
When the data received within the app needs to be processed and execute some business logic, stuff like calculations, data reshaping and other internal to the app processes. This is the domain layer.
Isolating the domain logic is a great practice for building resilient systems that not only can scale but also are easy to work with and modify. More of the latter later.
The ports layer in my opinion is the part that causes the biggest amount of confusion in the whole concept of this architectural pattern. Let’s see if we can make some sense out of it.
As we've already said, one of the selling points of Hexagonal architecture is the fact that it can make our app domain agnostic. What that means is that our business logic should be decoupled from the specific tools and infrastructure that we use. In other words, our domain should not be dependant of the specifics of the Database we use.
Similarly, the domain should not know that we’re sending it data via an SQS queue. The port is the bridge that connects the domain with the adapter and holds the logic that decides what information should be passed from one to the other. In typed languages a port is usually an interface that specifies the shape of the data that adapters have to pass to the domain and vice versa.
Let’s take a classic example where our application is a RESTful API which receives data via a HTTP POST endpoint and stores it to MongoDB. The journey would look like that
Our HTTP adapter will process the HTTP POST request and send the data to the port which will communicate that to the domain. This is where our internal logic will be executed. Stuff like internal calculations, reshaping data etc.
Then we need to follow the same logic in reverse. The domain has some data, wants to store it in the DB and has to send them to the repository port which will then send it to the Database adapter.
|Adapter (input)||HTTP handler|
|Port (to domain)||HTTPHandler.retrieveData|
|Domain||Process data and send data to repository|
|Port (to adapter)||Repository.storeData|
|Adapter (output)||Connection with MongoDB|
You’re probably wondering “yeah that’s cool but why would we go through all that trouble?”
To put Hexagonal architecture into a business perspective, it is painless when we want to introduce new features due to the loosely coupled way of structuring our code. We can change parts of our app without causing major disruption.
In addition to that, our future selves will really thank us when it comes to debugging an error, as we will immediately know where to look.
Did the app return the wrong data? That sounds like an issue in the domain layer. Was there a network issue during that request? Sounds like an adapter issue.
One of my favourite parts of Hexagonal architecture is that testing our code becomes much simpler. We all have experienced codebases that are really difficult to test due to their lack of boundaries where all of the implementation is just thrown into a function / method / class / whatever you want to name it, that is 100+ lines long.
With Hexagonal architecture each layer is a separate module we can test in isolation. This can be done by mocking its communication with other layers, which gives us the flexibility to have smaller tests that are easier to write and faster to execute. Bonus point, that can then result with higher testing coverage.
The whole concept of “plug and play” adapters is great because it ensures our business logic does not rely the tools we use.
The more specific our business logic is to a certain infrastructure, the more difficult it will be for us in the future to move away from this infrastructure.
How many times have we had to spend days if not weeks trying to find out how to switch from Database A to Database B because our code is too tighly coupled to Database A. This tools logic leakage inside our domain is something we need to be careful about.
Hexagonal architecture guides us on how to have clear boundaries between the tools and our business logic. Then once we decide to move away from a tool, it should be as simple as adding a new adapter.
Obviously I am not saying that migrating away from tools is going to be a piece of cake, but the transition within our app, will probably be the smallest of our concerns.
For those of us working with Serverless apps, it is a known problem that we very have our entire logic inside the handler. Then slowly once our application starts getting bigger and bigger, we either end up with gigantic handlers or some weird structure which looks like that infrastructure logic is mixed within the business logic. This is where we need to introduce some boundaries and Hexagonal architecture can help us with it.
I have to admit that the first time I tried to write some code using this pattern, it felt really weird. I think the biggest issue was not really understanding what kind of problem Hexagonal architecture is trying to solve. With time though it started making a lot more sense, and since then it has been the number one choise of structuring projects I've been working on.