Starting With Hexagonal Architecture
Hippolyte Morillon8 min read
How to start with hexagonal architecture ?
The Hexagonal architecture follows the principles of Domain Driven Design (DDD), aimed at crafting a backend system that is easily editable, flexible and testable. If you are unfamiliar with DDD, there is an article talking about what is Domain Driven Design and its avantages.
Assuming you’re already acquainted with Hexagonal Architecture, here’s a brief recap accompanied by a diagram.
The Domain encapsulates all business logic, isolated from the remainder of the application, the business logic can be manipulated without needing to take into account the infrastructure of the project. Adapters and ports facilitate accessibility of the business logic from the frontend and other APIs, as well as enable interaction with databases and external API routes.
Why and when should you start hexagonal architecture ?
Hexagonal architecture, and more generally Domain Driven Design (DDD), becomes profoundly compelling when dealing with complex business logic. By isolating the business logic from the broader application structure, it offers clarity and agility. This separation enables swift comprehension of the application’s functionality and simplifies modifications. Notably, it serves as an excellent learning tool for newcomers to grasp the workings of web applications by dissecting and elucidating various components. By the way, there is an amazing article on Why the Hexagonal architecture is beneficial for beginners.
Conversely, setting up everything for Hexagonal architecture requires additional time, as it involves creating entities and interfaces essential for its implementation. Therefore, if the application lacks complex logic, adopting this approach may not be advisable, as it could increase the weight of every route unnecessarily. Hexagonal architecture emphasizes the domain, thus it’s most beneficial when the domain is substantial. If your business logic is lightweight, the advantages of this method are less apparent.
For instance, in a basic blog application, the operational flow is straightforward: fetch posts from other users stored in the database and publish posts. In such a scenario, the domain plays a minor role since the application’s logic is minimal, making its separation from the rest of the code redundant.
Traditionally, to implement a route for retrieving a post from another user, one might use three classes in a classical architecture: the controller, the service and the repository for fetching data from the database. However, with hexagonal architecture, this would entail creating four files: the controller, the command, as well as the port in the domain, and the adapter in the infrastructure. The additional domain files serve merely as intermediaries for relaying information, thus appearing useless.
On the contrary in cases where business logic is notably dense, such as in a banking application with complex validation principles and transaction, adopting a hexagonal architecture becomes significantly more advantageous. With the complexity of the business logic heightened, the domain layer will inherently occupy a larger portion of the overall structure compared to simpler applications. Consequently, the additional time invested by developers in implementing extra files for this architecture will facilitate a proper segregation of the business logic from the rest of the system. This separation enhances clarity for future development endeavors, making the codebase more comprehensible and maintainable.
My team implementation of hexagonal architecture
My team and I embarked on a fresh endeavor to initiate and oversee contracts. Tasked with revamping the entire API, we opted to rebuild from the ground up, leveraging Spring Boot and adhering to the hexagonal architecture principles to craft a new API that would be easily maintainable and thoroughly tested. Within our team, we had two rookies who were unfamiliar with domain-driven design, alongside a tech lead. The aim of this article is to outline our journey, detailing our approach and highlighting the mistakes that ultimately impacted our development timeline.
In the approach my team had to setup our api, the code was divided into three main components:
- Domain: This segment houses all the business logic.
- Presentation: Here, all controllers are implemented and backend routes are exposed.
- Infrastructure: This part manages all communication with databases and external APIs.
When implementing changes, we followed a systematic approach. We began by updating the domain if needed, then proceed to the infrastructure, and finally adjusted the presentation layer. This ensures that our modifications originate from the core business logic.
For instance, to create a route for saving a User
in our application, one would follow these steps:
- First create a user folder in the domain and create the
User
entity with its value objects : - Then create the Command that would save the
User
, using theUserPort
in the user folder : - Then create the
UserModel
, make the migration, implement a mapper fromUser
toUserModel
and implement theUserPort
by creating theUserAdapter
: - Only then create the route that would call the Command to save a new
User
by creating theUserController
and theUserPresentationMapper
to convertUserDto
toUser
:
Here’s how the application should appear at this stage:
All modifications stemmed from the domain. Initially, we would update the entities, if required, followed by enhancements to the domain. This involved creating commands and logic, and subsequently, the port. After aligning the infrastructure with the domain’s logic, we concluded by implementing the route.
Initially, we mirrored the schema of our database to create domain entities, as the logic closely paralleled. Subsequently, adjustments were made as per the specific requirements of our domain.
By adhering to these principles, we were able to develop a robust backend with intricate business logic, easily adaptable to change.
Key errors to avoid when deploying a backend following hexagonal architecture
When Infrastructure enter the Domain
It is crucial to avoid importing your Infrastructure into your Domain. The Domain should remain independent from the rest of your codebase to truly adhere to Domain-Driven Development principles. This segregation is vital because the business logic, often complex and unique to the application, needs to be distinct. By isolating it, testing the business logic becomes much more easier. Also developers can comprehend and modify it more efficiently without being encumbered by the intricacies of the infrastructure
While Infrastructure and Domain may share some proximity, particularly during entity creation, caution is necessary. In our project, we initially utilized the SQL schema to inform the creation of entities in our Domain. This approach worked initially due to the alignment between the schema and the business logic. However, as the project evolved and business complexity increased, discrepancies emerged between the two. Concurrently, we observed instances where entities began to rely on models or classes from other APIs.
Consider the following example. This code serves as a postal code validator specifically designed for a given domain. The function assertPostalCodeValidityForFrance
is intended to validate that when both a country and a postal code are provided as arguments, they align with the application’s logic. In cases where the provided country is not France, the postal code must be null. Additionally, there’s a dependency on a ThirdPartyApi, which is another API utilized within the infrastructure. The ThirdPartyApiService
, found within the infrastructure, implements the same enumeration as the ThirdPartyApi
:
public class PostalCodeValidator {
public static void assertPostalCodeValidityForFrance(Country country, PostalCode postalCode) {
if(country.value().equals(ThirdPartyApiService.FRA_ISO_3_CODE)) {
// do something
} else if (postalCode != null) {
throw new PostalCodeInvalidForForeignCountryException(postalCode.value());
}
}
}
In this scenario, there’s an issue with the enum being sourced from the infrastructure, which itself relies on another API. The primary concern is that any alterations to this external API’s enum, even if unrelated to our application, would necessitate changes in both the domain and business logic. Over time, developers may forget the context of this implementation, leading to potential confusion. Furthermore, if the enum values change while the structure remains the same, our application could encounter failures.
A viable solution is to develop an enum within our domain tailored specifically for our business needs. Additionally, we can implement an adapter to map the enum values from the external API to those within our domain. This approach enables us to promptly identify and address any modifications to the external API’s enum, thereby mitigating potential bugs while keeping our business logic unchanged.
Don’t miss the Records !
In Hexagonal architecture, you’ll find yourself creating numerous value objects and entities, each with similar logic across them. To streamline this process, you can leverage existing types for instantiation, such as Java records. Records in Java come with all the necessary methods for a value object, simplifying their implementation.
@Getter
@AllArgsConstructor
public class UserName {
@NotNull
String value;
}
VS
public record UserName(@NotNull String value) {}
With the complete logic, the code extends to almost 5 lines, whereas a record condenses it to just 1 line. Moreover, records offer added advantages by implementing hashAndEquals, facilitating comparison of value objects without accessing their values, especially in test where expected and real values are compared frequently.
Similar types are available in other languages as well. For instance, C# also features records.
To wrap it up
When embarking on the implementation of a hexagonal architecture from scratch, explore tools that streamline your workflow, such as new types that can swiftly implement DDD entities.
Furthermore, during the coding process, consistently question whether the changes being made are on the business logic and therefore should belong to the domain or not and whether the entities being utilized are indeed part of the domain. Always bear in mind not to invoke any entity external to the domain within it. The domain must consistently remain isolated from the rest of the application. It should be feasible to strip away everything except the domain and have it function autonomously.