August 29, 2024

How to build a modular monolith with Hexagonal Architecture?

In software development, designing a system that is both scalable and maintainable is crucial. One architectural style that has gained popularity for achieving this is the Hexagonal Architecture, also known as the Ports and Adapters architecture. This style aligns closely with other architectural paradigms like Uncle Bob’s Clean Architecture and Onion Architecture, particularly in the way they manage dependencies and the direction in which they flow.

Understanding Hexagonal Architecture

Hexagonal Architecture, introduced by Alistair Cockburn, is centered around the idea that the core business logic of an application should be isolated and independent from external concerns like user interfaces, databases, or other external systems. This core is often referred to as the “domain” or “application” layer, and it interacts with the outside world through well-defined “ports” and “adapters.”

  • Ports are interfaces that define the communication between the core and the outside world. They represent the entry points and exit points of the application.
  • Adapters are implementations of these interfaces, connecting the core to external systems (e.g., databases, APIs, user interfaces).

The Principle of Dependency Inversion

A fundamental concept in Hexagonal Architecture, and one that is also central to Clean Architecture and Onion Architecture, is the Dependency Inversion Principle (DIP). This principle dictates that:

  • High-level modules (the core business logic) should not depend on low-level modules (like databases or UI frameworks). Instead, both should depend on abstractions (interfaces).
  • Abstractions should not depend on details. Details (concrete implementations) should depend on abstractions.

This principle ensures that dependencies only flow in one direction: from the outer layers (infrastructure, UI) to the inner layers (domain logic).

Uncle Bob’s Clean Architecture and Onion Architecture

Uncle Bob’s Clean Architecture and Onion Architecture share the same core principles as Hexagonal Architecture. They both emphasize a clear separation of concerns, with a focus on keeping the business logic at the core, unaffected by external changes. In these architectures, the application is structured in concentric layers:

  • Inner Layers: Contain the core business logic (entities, use cases). These layers are independent of external systems.
  • Outer Layers: Contain frameworks, databases, user interfaces, and other external systems. These layers interact with the core through interfaces.

In all these architectures, the critical point is that dependencies flow inward, and inner layers have no knowledge of the outer layers. This is what makes the system robust, testable, and adaptable to change.

Modular Monolith with Hexagonal Architecture

A modular monolith is a single application divided into distinct, independent modules, each responsible for a specific domain or feature set. By adopting Hexagonal Architecture in a modular monolith, each module becomes self-contained and follows the principles of ports and adapters.

1. Core Domain Logic:

  • Each module has its own core, which contains the business rules. This core is agnostic of the external systems, making it easy to test and maintain.
  • The core exposes ports (interfaces) for any external interaction.

2. Adapters:

  • Adapters are implemented on the periphery of each module. These could be repository implementations for data storage, APIs for external communication, or UI frameworks for presenting data.
  • Each adapter interacts with the core through the ports, ensuring the core remains decoupled from the external systems.

3. Dependency Flow:

  • The flow of dependencies is strictly from the outer layers (adapters) towards the inner layers (core logic). This ensures that changes in external systems or frameworks do not affect the core business logic.

Leveraging Hexagonal Architecture for ARTis

In developing the ARTis backend application, we have chosen NestJS as our framework. This decision allows us to effectively implement Hexagonal Architecture, ensuring that our application is modular, maintainable, and scalable.

In our setup, the DTOs (Data Transfer Objects) and controllers form the Input Adapters layer of the application. This layer handles user interactions and external inputs, acting as the gateway through which data enters the system.

Our application logic is segregated into commands and queries, which are organized within the Application Layer. The controllers invoke the appropriate command or query through their run method, enabling the application to handle specific user requests or use cases.

Each module within ARTis is treated as a bounded context, meaning it has its own set of controllers/APIs (adapter layer), commands, queries (application layer), and entities (domain layer). This encapsulation ensures that each module maintains its own domain logic and can operate independently.

To facilitate interaction between modules, we have designed connectors—intra-module communication APIs. These connectors ensure that only code within a module can access its related entities, maintaining the integrity of each bounded context.

Furthermore, when a module requires data from another module, it calls the connectors API of the other module to retrieve the necessary information. This design provides us with the flexibility to migrate to a distributed or microservices architecture in the future if necessary.

By leveraging Hexagonal Architecture in this way, ARTis is not only well-structured for current needs but also poised for scalability and evolution as the project grows.

Conclusion

By adopting Hexagonal Architecture in a modular monolith, you create a system that is modular, testable, and easy to maintain. The key takeaway is the importance of the Dependency Inversion Principle and ensuring that dependencies flow from the outside in. This principle, common to Hexagonal, Clean, and Onion architectures, allows you to build systems that are resilient to change, scalable, and adaptable to future requirements.

Using these architectural principles, your modular monolith can evolve into a system that not only meets current needs but is also prepared for future challenges.

You may also like