A cleaner version of the hexagonal architecture
Photo by Soloman Soh: https://www.pexels.com/photo/architectural-photography-of-glass-buliding-1492232/

A cleaner version of the hexagonal architecture

If you are a software engineer, like myself, chances are that you have already gotten in touch with something called hexagonal architecture.

In case you haven't , here's a brief introduction about the subject:

It's a type of software architecture created by Alistair Cockburn (the same creator of the Crystal Clear methodology), which aims at decoupling the implementation of the domain from its inputs and outputs - like most other types of architectures known in software engineering.

What hexagonal architecture has that's different from other architectures is that it specifically names the indirection system it provides as ports and adapters - which, by the way, it's the nickname of the architecture itself.

Let's have a look into the Wikipedia image for hexagonal architecture, in order to understand it a bit better:

Example of hexagonal architecture. Source:

As you can see, at the center of the hexagon lies all of the domain logic. If you create a software that handles balances of customers for a bank , this is where you write the logic about what happens if the customer wants to send money to another but has not enough funds.

In the edge of the hexagon are all adapters. Everything that communicates with the external world is an adapter , and the application logic communicates with adapters through ports. In the case of the banking application, a port contains the definition of how to save the state of the balance, and a database adapter is the actual mechanism that defines how to save it.

If you work with a language that has interfaces, you may think of ports as interfaces, and adapters as implementations of these interfaces.

And that's precisely where it starts to get complicated.

How is hexagonal architecture implemented in the real world?

In order to create software that follows the hexagonal architecture and is truly decoupled, you need to bear in mind that ports are meant to be internal API's within the system. To have an API means that the user does not know (or should not know) anything about the implementation. It's more or less like Liskov's substitution principle, but instead of classes and superclasses, think about interfaces and implementations. I'll give you an example:

In Java, we have List (which is an interface), and ArrayList (which is a class that implements List). As such, we can have code like:

So, the code above creates a List (using ArrayList as an implementation), adds a customer to it and then, prints all customers it contains.

Aside from ArrayList, we also have LinkedList. In order to use it, we can have code like:

The difference in usage between them is none. The purpose of having both of them is that they are implemented in different ways, and could have different performances depending on how much they would be read/written . Often times, they are created in sections of the software and used in other sides, without the users ever knowing what's the actual implementation. A good API offers this: you can replace implementations and users don't even notice.

The purpose of having ports and adapters is exactly the same: in case you ever need to replace an adapter, the domain logic should not ever need to be changed. Only the adapter itself, which, in theory , could simply be replaced without harm .

So let's consider our banking domain, and hexagonal architecture. Considering that ports are interfaces and adapters are the implementations, let's have a look at a very simplified version of the mechanism that persists customers in the database:

The purpose of each class being:

  • The CustomerService would be where the business logic would be implemented. If you have a logic that says that there must not be two users with the same document numbers, that's where such a logic would need to be implemented.

  • The Customer class is the one that would hold the data. Think about it just as a data holder. If you use DDD, that would also be the place where some basic logic concerning only customers would also be implemented.

  • CustomerPersistencePort is, as already mentioned, the interface to the database. Bear in mind that it does not contain any references to the actual database implementation at all - which means, that it only receives objects of type Customer as parameter.

  • CustomerEntity holds the actual mapping to the database. If you use Java and JPA, this is the class that holds all references to packages jakarta.persistence .

  • CustomerMapper is the class that takes care of data transference between Customer's and CustomerEntity's

  • CustomerPersistenceAdapter is a class that implements the CustomerPersistencePort. It takes in customers, call the CustomerMapper in order to have entities and then calls the CustomerRepository...

  • ... which is basically for performing the actual persistence. If you use Spring Data, you only need to have an interface here, and then Spring Data will perform injection of an implementation on the fly.

Where the problem lies? In details, of course.

Now let's pretend users are saved in SQL databases, and their ID's are generated by database sequences. I call these as details because, just by showing the classes, it's not possible to see them - one needs to zoom in .

As the benefit for hexagonal architectures would be about being able to change implementations without changing the domain layer, let's pretend we need to change the database implementation. Let's pretend we are replacing Postgres by MySQL.

If you are using Java or any other modern language, chances are that you don't need to change any classes at all. Not even a single line. That's because that's mostly a matter of configuration - you just need to change the JDBC connection string (or whatever connection string you have), username, password, driver and.... that's it.

Without using hexagonal architecture it would be.... exactly the same. Even if you had the CustomerService to directly access the database without any indirection layers at all between them, you still wouldn't need to change anything.

But let's make it a bit harder then. Pretend that, instead of Postgres, you are migrating to some NoSQL database.

In this case, most likely your database wouldn't be able to provide you sequences anymore, therefore forcing you to choose another mechanism, like assigning the classes UUID's (or GUID's).

But wait. In order to be able to later retrieve the data from the database, chances are that the domain layer needs to know the ID datatype, right? Meaning the Customer class needs to have a reference to the actual entity in the database, once persisted. Of course, in this particular case we could have natural ID's , like the customer document, but that's not the case with several other real-world scenarios.

So this is an example of, by changing the adapter, there's a chance you would need to change the domain logic as well. It's not possible to shield the domain from everything.

There are even more complicated scenarios, of course. Often times we need to carry out data that comes from a specific adapter to other adapters. It's always possible to create yet another hexagon that would hold references to those others, but this would just keep getting more and more complicated, by demanding the creation of several classes in the application just for the sake of not hurting the architecture.

So how to not have this kind of issue?

My proposal is based on something slightly different. An architecture that holds the basic ideas of the hexagonal architecture, but in a more straightforward way.

At its core, hexagonal architectures (and , actually, all architectures) are about access control between classes. Which class can access what.

But the hexagonal architecture also relies on the idea that it should be able to replace an adapter without hurting the domain - which, as I have shown above, it's either impossible to keep in the real world or, at least, can become very complicated.

So I created a different type of architecture, which I call IDO architecture.

In the one I created, there are basically three layers in the application (but an indefinite number of sub-layers).

We have the incoming layer, which is where the data comes into the application. Examples of components that would sit in the incoming layer are message listeners (like Apache Kafka or RabbitMQ listeners), REST endpoints, File polling mechanisms, etc.

We have the domain layer where, just like hexagonal architecture and other architectures, is where business logic is processed.

And we have the outgoing layer, which is there the date comes out of the application. Examples of components that would sit in the outgoing layer are database-related classes, messaging publishers, file writers, and components related to external API calls.

These layers are organized like a cake:

  • incoming, on top of

  • domain, on top of

  • outgoing.

The most basic rule here is that each layer can see the components that are within themselves and the ones that are in the immediate layer below them. But they cannot interact with components that are above their layers and cannot "jump" layers as well.

So, translating this rule, we have:

  • the incoming layer can interact with itself and the domain layer;

  • the domain layer can interact with itself and the outgoing layer (but not with the incoming layer);

  • the outgoing layer can only interact with itself, and no other layer.

But notice that there are no ports and adapters here. The idea here is that API's can be defined in classes directly - there's no need to define actual interfaces in order to have API's.

... and how does this solve the problem ?

By using IDO, we would describe the banking application like:

Here, if we need to persist a customer in the database, the CustomerService accepts the customer data (which is defined by the class Customer), and then we use the mapper to translate it to a CustomerEntity. Then, it calls the CustomerRepository to persist it .

So the rationale here is: a CustomerRepository cannot see the Customer, and that's why we need the mapper . But the customer service is free to access both the CustomerEntity and the CustomerRepository.

Let's revisit the problems from above:

When it comes to changing the entity ID type, there would be no much issues. The type would need to be changed in the class Customer and in the CustomerEntity - but chances that the same would be needed in hexagonal architecture would be high .

And when it comes to changing the database layer, the scope of change would be more or less limited to the package database.customers and, maybe, something in the CustomerMapper. But that's it.

The overall idea is that software architecture must be very, very pragmatic. It should help in case of needed replacements, but it should not be in the way of changes, making them as small as possible whenever they are needed.

So we define API's in classes themselves, assuming that creating interfaces and actually having good API's are different things.

What about a more concrete example?

Let's extend the idea of the banking app to a scenario where it receives the customers data through a REST controller and then, after persisting them on the database, also calls another system for further processing. Under IDO architecture, it would have the following structure:

In the incoming layer, there's a sub-package called http . There's a Controller class that represents the REST call listeners, which hydrates an object CustomerDTO. Bear in mind that this class needs to contain information specific to how it would be hydrated, like JSON or XML-specific annotations.

Once it receives this call, it would call the CustomerDTOToDomainMapper, which would translate this DTO to a Customer object. Once more, notice that this object would contain no annotations that would be specific about it's incoming protocol - so it could have been created through JSON, XML, binary, whatever.

Then the CustomerService would take this customer data and use the mappers CustomerToDatabaseMapper and CustomerToSysABCMapper in order to translate it to CustomerEntity and SysABCCustomerDTO, respectively. Then it would call the appropriate components in the outgoing layer.

This example is particularly useful to outline one more rule of the IDO architecture: no sub-package within a layer can access another sub-package within the same layer. For example, in the outgoing layer, sub-package database.customers we have the class CustomerEntity . No classes present in the sub-package http.sysabc can access CustomerEntity directly; it's forbidden. The same would apply in the incoming layer if we had, for example, a messaging layer that would receive data from Kafka. It would be forbidden to use the CustomerDTO that's defined in the http incoming layer there; another DTO would need to be created, as well as a new mapper.

Note that the same does not apply to classes within domain layer, although is advisable to at least try to enforce the same rule there.

Conclusion

The IDO architecture is basically just a reread of the hexagonal architecture. At the same time it tries to keep the concepts of promoting the isolation between components that interact with the domain, it also attempts to remove some of the complexity that's inherent to it, by no longer having the concepts of ports and adapters but, instead, trying to be more straightforward about the way classes communicate to each other.

One heavy motivator to it is the fact that, by using or not using the hexagonal architecture or any other architecture, changes in any kind of components usually affect the domain layer somehow - if not directly, at least the business logic usually needs to be changed somehow in order to accommodate new requirements that come together.

The IDO architecture then acknowledges this and does not turn away - instead, it just tries to minimize the number of classes and interfaces in the system, in such a way that changes are easier to be implemented by the developer.

Rafael Azevedo

Analista de desenvolvimento sênior no Grupo Casas Bahia | Especialização em Engenharia de Software

4mo

Qualquer arquitetura é melhor que hexagonal rs

Like
Reply
Alessandro Dias

Software Engineering Manager @ AGCO / Masters in Business and Administration

5mo

Great, a lot of similarities with Clean (Onion) Architecture! So I was IDOing all this time!

To view or add a comment, sign in

Others also viewed

Explore topics