Building Hexagonal Architecture for Scalable Solutions
Hexagonal architecture, introduced by Alistair Cockburn in 2005, aims to structure applications for isolated development and testing, free from external dependencies.
The traditional approach encounters issues in large-scale applications like:
1. Front-end Challenges: The application’s business logic tends to seep into the user interface in the front end, leading to challenging testing due to its tight coupling with the UI. Consequently, this logic loses its versatility across different use cases, making transitioning from user-driven to programmatic scenarios complex.
2. Back-end Concerns: Business logic coupled with databases and external services, impedes testing and technology stack transitions.
To overcome these issues, we can use a layered architecture:
The image above depicts the layered architecture. In this model, components can only access those within or below their layer, theoretically preventing the mixing of concerns. However, the absence of a clear violation detection mechanism often leads to the very issues we aim to avoid.
Placing the data access layer at the bottom lets the database dictate design. Instead, starting with the business logic might be more effective.
Entities often seep into higher layers, necessitating business logic adjustments when modifying persistence methods.
The above perspective is overly simplistic and seldom holds. In practice, the need to interact with external services or libraries complicates the determination of their rightful place.
What Is Hexagonal Architecture?
As previously noted, the core concept of hexagonal architecture revolves around isolating the business logic from external elements. All the business logic is confined within the application, while external entities reside outside of it. The application’s internal workings remain oblivious to the external environment.
The objective is to enable equal control of the application by users, other software, or testing procedures. This necessitates the capability to develop and test the business logic independently, without entanglement with frameworks, databases, or external services.
Hexagonal Architecture revolves around ports and adapters.
Ports: To achieve the division between business logic and the external environment, the application solely utilizes ports for communication. These ports define the nature of the interaction between both sides, with the application remaining indifferent to the technical intricacies underpinning them.
Adapters: Adapters establish connections to the external world and translate external signals into a format that the application comprehends. These adapters exclusively communicate with the application through the designated ports.
The outer layer of this diagram shows the adapters, depicted as the transport layer, where we keep all controllers catering to requests from the outer world and propagating them outward. The next layer is the ports, which wrap all interfaces required for the adapters. Finally, we have the configurations and models for the ports, adapters, and use cases. The use case at the core contains the business logic.
Cons of Hexagonal Architecture :
Let’s look at the cons of using Hexagonal architecture.
- Complexity: Introduces initial complexity with multiple layers and interfaces.
- Learning Curve: Developers may need time to grasp the concepts and adapt.
- Overhead: Adds code and configuration overhead.
- Over-Engineering: Risk of excessive layers in simple projects.
- Initial Development Time: Setup can delay speed-to-market.
- Legacy Code Compatibility: Challenging to retrofit into existing systems.
- Maintenance: Requires ongoing attention to avoid codebase issues.
- Team Familiarity: Resistance and training are required for unfamiliar teams.
Conversion of Existing Architecture to Hexagonal Design:
We have an MVC-based layered architecture, where the BFF (backend for the front end) exposes controllers consumed by the FE (front end).
Besides that, the implementation layer consumes the clients to call the external APIs. So this shows the seepage of business with the external agency collaboration. No isolation at all. Based on that test scripts and unit tests were there. Which are directly connected with the tech stack and strongly coupled with the business logic. Hence to change any of the external agencies or the configuration related to that implies change at the other ends as well.
Now let’s see how we leveraged the above-mentioned pattern in MAVI. For the Customer Master Data microservice, we created a POC with the structure shown below in the image:
1. We categorized the outer boundary as adapters with all possible points of connection with external agencies. It comes in two parts: first, where we handle incoming requests (inbound adapters), and second, where requests are routed to external APIs (outbound adapters). For example:
Inbound Adaptor: Customer Controller that has having endpoint to Master Data microservice UI
Outbound adaptors: For outbound we have created a sequence of an adaptor having the HTTP client for downstream API for each external API required as per the contracted version (like geoaddressmanagement, communication, onboarding, partymanagement etc.)
2. So we have divided the exposed APIS and clients into the mentioned parts.
3. And similarly we’ve created the ports, which are also functioning in the same inbound and outbound ports depending on catering for the request. For example:
Inbound Ports: supporting the inbound adaptors like customerAdressPort, CustomerCommunicationPort, CustomerContactPort, CustomerPort, and ConsentPort.
For Outbound Ports: AddressManagementPort, CommunicationManagementPort, OnboardingPort etc.
4. Now Coming to the use case (or actual business implementation) we have kept services here which are supporting the inbound port and stimulating the outbound port. For example CustomerAdddressServive, CustomerCommunicationService etc.
5. To support the use cases we have added two packages for keeping constants and models.
Now looking again at the structure/architecture diagram of the Master Data microservice will be more meaningful.
Let’s look into a single flow out of this whole master data microservice to make it more meaningful:
1. Let’s say the Customer Master Data Frontend calls the Inbound adaptor i.e. Customer Controller,
2. To fulfil that first an inbound port i.e. Customer Address is being called.
3. Which later redirects to the use case where business implementation is present for fetching the Address-related metadata and enriching with the other responses.
4. So a downstream call to the external API is required which is being carried out by an Outbound port i.e. AddressManagement port. This calls the downstream API and returns the response to the use case.
Realized Benefits:
1. Improved Testability: Emphasis on separation of concerns, made it easier to test individual components in isolation. This can lead to more robust and maintainable tests.
2. Loose Coupling: Reduced dependencies between different layers of the application.
3. Business Logic Isolation: Core business logic at the centre, which is isolated from infrastructure and presentation concerns.
4. Future-Proofing: By enforcing clean boundaries and isolation between layers, helped the application more adaptable to future technology changes and requirements without adapting changes and interference to connected layers.
With hexagons in our codebase, we’ve gone from ‘shapeless’ to ‘shapely’ in no time flat! Who knew geometry could be this entertaining? Cheers to coding with corners!