Within C#, we use a wide range of architectures, designs, and patterns. One of the most popular architectures is hexagonal architecture, which we’ll also call ports and adapters due to its structure.
1 - What is hexagonal architecture?
We call it hexagonal architecture because there’s a central core where the business logic resides, and surrounding it there’s another layer: the ports and adapters, which allow us to interact with external services such as third-party services, the database, the file system, or the interface itself.
A key aspect of hexagonal architecture is that the core (or whatever you want to call it; the business logic) does not know how the data is processed or received, but all the rules and processes are in a single place. This greatly simplifies testing and development.
Now it’s time to explain ports and adapters.
We can think of ports as interfaces that we inject into our business logic. These interfaces contain the contract that the logic will use.
An adapter is nothing more than the abstraction, not the implementation, of that port—the implementation itself is completely transparent and irrelevant to the use case.
In my opinion, using the terms "ports and adapters" just complicates things, when it’s really just interface and implementation. But anyway, these are minor details.
2 - Features of hexagonal architecture
As with everything in programming, there are benefits and downsides.
From my perspective, there are two disadvantages:
The first one is obvious: to properly implement hexagonal architecture, you must create ports and adapters, which leads to a lot of code. Another important point is that you need to clearly separate Entities and DTOs.
For example, I’ve seen the following: You read a user from the database using Entity Framework, convert that user to a DTO, modify it, change a value, and save it again. But here’s the problem: you’re sending a DTO to the database adapter, not the Entity Framework entity, so you have to reread the entity to save it.
Also, this particular company would return the updated user to the user as a response, which implies another conversion (although you could return an IUser interface).
I think this example clearly illustrates my point: there are obviously extra steps when this particular process could be done in four lines of code.
The other downside—though this depends more on the company—is that all the business logic is located in a single file. They create a file called UsuarioRepository, and all logic related to users is in there. The problem is that all actions are in the same file: reading, updating, deleting, validating, etc.
Note: in this case, UsuarioRepository is the user logic, not data access. Yes, naming issues could be a post on their own.
And that’s if it’s even split up. Many times the entire app is in a single file, for example, AppRepository, and everything related to users is in there. If your app is about books, everything about books is in that same file. You eventually end up with a three-thousand-line file that could be split very quickly and easily, but nobody wants to for fear of breaking something.
If, instead of having everything in a single file, we separate the use cases, we’ll drastically improve the application, because we’re already separating responsibility with ports and adapters, so why not separate the use cases too? This way, when another developer wants to make a change or implement something new, they only need to focus on what they’re doing.
If we put together everything above, we realize testing is very easy—or at least it seems so at first—since all dependencies are behind interfaces, which means we can easily create a test double.
Testing hexagonal architecture is very simple as long as we have separate use cases, since we only inject the ports (interfaces) used in that use case. But if we have a God Object, we’ll be injecting many ports we don’t need for each use case.
If there is any problem you can add a comment bellow or contact me in the website's contact form