Have you ever wondered how you could isolate your application from external concerns, like which database to use? Or isolating it who is consuming your application?
Domain-centric architectures, like the Ports and Adapters, allow you to achieve just that.
The Ports and Adapters architecture, also known as Hexagonal architecture, was a concept started by Alistair Cockburn in 1994, but then officially written in his blog in 2005.
Its intention is:
Allow an application to equally be driven by users, programs, automated test or batch scripts, and to be developed and tested in isolation from its eventual run-time devices and databases.
This connected to what we previously stated: isolating the business logic from everything external and that can be ever-changing.
Our business logic is the money maker, so we must protect and isolate it from external dependencies. It should not be coupled to anything.
The reason why you should care about this decoupling is because you should consider these external factors as ever-changing. These should be easily replaceable. You don’t want to change your business logic just because your database type has changed, or because some application no longer is consuming your services via HTTP and now uses gRPC. You want your application to be tested, maintained, and evolve independently of such factors.
What is the Ports and Adapters architecture
As stated previously, the Ports and Adapters architecture is an architectural style that aims at decoupling the business logic from external factors.
It’s a use-case-driven architecture, where you first think about the use cases, and then only about the implementation details like which database to use. By doing this, you could have your application running all in-memory, with in-memory dependencies, which is something that is very powerful when you think about it.
Hexagonal architecture is not prescriptive on how you organize your code inside the hexagon. This makes applying this architectural style much easier, as the focus is on the isolation factor.
Here’s a visual overview of this pattern, before we proceed to explain each individual part:
Let’s now address some of the most common terms associated with this pattern.
-
„The Hexagon“: Before we go into details about what this is, first let us understand why the hexagon is the chosen shape. The reason is that, back in the day, every architecture diagram was using squares, and Alistair wanted a symmetrical shape that was not already taken and that could have room to insert ports and adapters freely.
Now, as far as the purpose of the hexagon, this is where your core business logic is located, with your use cases and, possibly, domain constructs like domain entities, and so on.
If you look at the architecture diagram above, you’ll notice that everything that is on the left-hand side is called Driver X, where X is the element you are referring to (ex. driver adapter). The reason why it’s called Driver is because it’s the element that is related to the usage of our application.
The right-hand side is called Driven X. It’s called this way because it’s what the application uses.
You must be wondering: „Sure I have my business logic here, but how can I call it from the outside world? And how can I make use of databases or message brokers?“. This is where the concept of ports comes in. -
Ports: Ports are interfaces defined by the hexagon. Adapters outside the hexagon implement these interfaces (for driven ports) or consume them (for driving ports).
We have two types of ports: the driver ports and the driven ports.
In the case of the driver ports, they are used to call the use cases, for example, call a use case to debit from someone’s bank account.
The driven ports are used by our application to call the driven actors. For example, this port represents the contract that stores, retrieves, updates, and deletes objects from a database.
You can organize the ports by their intent. For example, you can have a group of ports for notifications and another group of ports for administrative tasks.
A misconception is that, because it’s a hexagon, it means that we must have exactly 6 ports. This is something that is not correct. You can have as many ports as you want.
So, now we have defined the contracts to interact with our application (and to access the outside world). Now comes the question: who uses such contracts? This is where the adapters come into play. -
Adapters: These are the middle elements between the actors and the ports. The driver adapters must be coupled to a specific actor. For example, the driver adapter must be coupled to driver actors, so that they can convert the data they send into data that is compatible with the inputs of the use cases. On the other hand, the driven adapters must convert the data they get from the application to what the driven actor understands, and vice-versa.
-
Driver actors: Elements that are interested in using our application via our use cases. For example, a user in some Web portal performing administration tasks, or even a message broker is trying to push messages into our application.
-
Driven actors: Elements that our use cases use, like databases or message brokers.
Conclusion
Enabling domain-centric architectures is always a safe bet when it comes to building resilient and adaptable software.
Ports and adapters are just one way of contributing to this purpose.
Want to see this in practice? Stay tuned for the next posts!