Hexagonal architecture and how to prevent technical debt

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:

  1. Update the API to accept and process the telephone number.
  2. Update the database schema if needed to store the telephone number.
  3. Add validation and security (e.g., sanitise inputs).

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.

Article content

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.

Article content

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:

Article content

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:

Article content


And finally, the full implementation is done by consuming the UserService in the API:

Article content


In this example, the actions happen following the steps:

  1. The client requests the update using UserUpdateRequest interface.
  2. The API uses the UserService to interact with the business logic (User Entity) in the method update. The UserService method updates are expected input from UpdateUserInput interface.
  3. The Service method interacts with the Repository to retrieve the User, manipulate it, and persist. The UserService method returns an object with the UserOutput interface (via the ToResponse method).
  4. Finally, in the API, from the service update method return (UserFromService), another casting returns the response to the client, this time with the UserUpdateResponse interface.

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:

  • The UserUpdateRequest interface might include raw input data from the client (such as unvalidated telephone numbers).
  • The UpdateUserInput interface refines this data for the service's internal use, perhaps including validation logic or default values.
  • The UserOutput interface structures the data for consumption by external systems.

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

Like
Reply

To view or add a comment, sign in

More articles by Danilo Pereira

Others also viewed

Explore content categories