Hexagonal architecture and how to prevent technical debt
One of my recent hobbies has been to look at and comment on snippets that some developers post here (or on other platforms). Inspired by this, I decided to go back to writing technical articles (this time in English). This article is the second series episode where I want to share how I like writing and reviewing code.
Taking advantage of the momentum from the previous article and trying to maintain a productive writing routine, this article will extend code writing techniques. I decided to discuss one architectural pattern that will be fundamental for many other patterns that will be addressed in the future.
Hexagonal Architecture is one of my preferred software architecture designs because it allows code to evolve or be refactored without adding technical debt.
Developers often focus on completing tasks according to business acceptance criteria but overlook the broader implications for maintainability and scalability. Without a clear separation of concerns, code can quickly become an indeed code entanglement of interconnected layers, making changes extremely risky and time-consuming.
For example, when implementing a user story like "As a user, I want to add my telephone number to my profile," the most straightforward approach might involve:
The example above (suggested by ChatGPT) doesn’t consider any architectural pattern. As a result, the controller's and business logic's responsibilities are not isolated.
While this might meet immediate goals, this approach tightly couples the API and database layers. Any future change, such as altering user data storage or retrieval, would ripple through all layers. This is where Hexagonal Architecture comes in.
Hexagonal Architecture in Practice
The core concept, which is also applied to many patterns, is about isolating business logic in a separate layer; this allows for easier maintenance, testing, and adaptability to changes in technology or business requirements.
So, first, we need to define and isolate our business logic (domain). For this, let's create the User entity. Optionally, we can add the entity setters to the required validations related to business logic.
The Hexagonal Architecture ensures that the business logic layer only communicates with the other layers through a port and adaptors (inversion of dependencies strategy). So, any resource that interacts with business logic (User entity or service, for example) must include a port. A port is an interface that defines the contracts of any interaction with the business logic, and the adaptor implements it.
Below is the Repository as an adaptor:
A service should be implemented to interact with the repository as well. This service will have the responsibility to interact with the User entity and could be consumed by the API handler:
Recommended by LinkedIn
And finally, the full implementation is done by consuming the UserService in the API:
In this example, the actions happen following the steps:
You may have noticed the repeated interfaces: UpdateUserInput, User, UserOutput, UserFromService and UserUpdateResponse. All these interfaces have the same shape, and you can ask why there is so much duplicated code.
Code duplication dillema
Implementing it may seem redundant since interfaces (and eventually methods) are commonly rewritten and present in many places in the code. Still, there’s a critical reason for separating these interfaces: Using multiple interfaces helps enforce clear boundaries between layers, improving modularity and reducing the risk of unintended dependencies.
Each layer should have a distinct responsibility. Using separate interfaces ensures that each layer only interacts with the data it needs, adhering to the Single Responsibility Principle:
By isolating these interfaces, you prevent unintended side effects when one layer changes. For example, if the client-side input format evolves, it doesn’t impact the service or database.
Suppose the database schema changes to store user details in a NoSQL format instead of a relational table. The Repository Layer can adjust its implementation without affecting the User Service or the API Layer, which still interacts with the same UserOutput and UserUpdateResponse interfaces.
Conclusion
Adopting Hexagonal Architecture requires more code and time but pays off in the long run. However, if your project changes (and it will), it's essential to ensure your code cares about:
Maintenance: Avoid rigidifying the system; each layer can evolve independently without affecting the other. This allows teams to work on different system parts simultaneously since each can work in other layers or distinct features.
Encapsulation: Different interfaces act as clear contracts between layers. Having multiple layers avoids overexposure of internal details, ensuring, for example, that business logic remains encapsulated and protected from external systems.
Debugging and Testing: Tests and debugging should be simplified by limiting each layer's scope and having the opportunity to mock the layers you will not test.
Hey Danilo... Do you have a public github repo that implements a well strucuture and large project using Hexagonal architecture? I just found several repos but implementing basic examples. I was looking for a complex and huge project to see how to manage the complexity using hexagonal architecture in microservices.... thanks