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.
- Adapters
- Ports
- Domain
Adapters
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.
Domain
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.
Ports
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.
A use case
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 |
Benefits of Hexagonal architecture
Youβre probably wondering βyeah thatβs cool but why would we go through all that trouble?β
1. Flexibility meets structure
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.
2. Isolated testing
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.
3. Domain agnostic app
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.
Conclusion
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.
Top comments (2)
I really like this article. To your conclusion, I think it's common for people to think that a specific lambda function is an event handler and therefore they must pile all this logic into a single layer.
In fact, the actual handler layer, the part that receives the event, should be really thin.
All the goodness of the hexagonal architecture can be modelled as layers (loaded language, I don't mean a Lambda Layer, I mean a code layer) that are simply triggered by the event the handler receives.
The lambda becomes a mini-hexagonal app in itself. We typically have the following
handler - receives and validates the event
services - apply business logic
data-access-layer - abstraction/port for accessing an external API
transit - connections to external APIs (databases, 3rd party systems etc)
We'll then have multiple handlers with an app, which typically share the data-access-layer and transit, sometimes services but they're all bundled separately so we get the benefit of treeshaking.
I never got notified for that comment so apologies for the comically late response.
I cannot agree more. I find that there's many different words for describing the same thing which different people understand them differently which ends up with confusion.
I have been aware of the domain driven design approaches for a while but Hexagonal architecture was my first hands-on experience with one of them. It slightly complicates things at the beginning but once you get the idea it makes perfect sense.
The multiple handlers that you mentioned is a perfect example of an approach that might be confusing once you first come across it but it is a great way of abstracting components with similar functionality and therefore using the same domain logic.