Many monolith architectures are transitioning to microservices, often to address the maintenance and management challenges that accompany the need for highly-scalable apps and continuous development processes. However, microservices architectures come at a premium, as they impose much higher operational complexity than the traditional monolith.
It often seems like architects will have to decide whether they want to sacrifice simplicity of management to enable streamlined, modular development. However, a concept known as the modular monolith may provide development teams the perfect balance between these two extremes.
Let's examine what a modular monolith is and the underlying details of how it works. Then, we'll examine some scenarios that warrant the use of a modular monolith, and some that don't.
Conventional monolithic architectures focus on layering code horizontally across functional boundaries and dependencies, which inhibits their ability to separate into functional components. The modular monolith revisits this structure and configures it to combine the simplicity of single process communication with the freedom of componentization.
Unlike the traditional monolith, modular monoliths attempt to establish bounded context by segmenting code into individual feature modules. Each module exposes a programming interface definition to other modules. The altered definition can trigger its dependencies to change in turn.
Much of this rests on stable interface definitions. However, by limiting dependencies and isolating data store, the architecture establishes boundaries within the monolith that resemble the high cohesion and low coupling found in a microservices architecture. Development teams can start to parse functionality, but can do so without worrying about the management baggage tied to multiple runtimes and asynchronous communication.
One benefit of the modular monolith is that the logic encapsulation enables high reusability, while data remains consistent and communication patterns simple. It is easier to manage a modular monolith than tens or hundreds of microservices, which keeps underlying infrastructural complexity and operational costs low.
However, development teams must understand that modular monoliths don't provide all benefits of microservices, particularly when it comes to diversifying technology and language choices. Since the applications need to execute code within a single runtime, there is limited opportunity for mixing those runtime environments.
These types of polyglot technology stacks are useful when legacy technologies create a performance bottleneck or if developers want the ability to work and experiment with the language of their choice. In cases like these, the modular monolith will likely fall short when it comes to meeting your architecture goals.
The right code design and layout are central to the modular monolith. Below, Figure 2 shows the code behind a movie ticket booking application that follows the modular model. The code has been structured with multiple feature modules and each one has an interface that represents its public definition. SeatMapService acts as the interface for the SeatMap feature. This interface is implemented by a class under the service package called SeatMapServiceImpl.
This code structure exposes the module interface in two ways:
Figure 3 diagrams the unidirectional dependency design, which is crucial for low-coupling in architectures. No matter how simple the application, architects must enforce a one-way dependency flow design. Since application code tends to grow over time, any multidirectional dependencies it possesses can eventually turn into troublesome dependency cycles. These circular dependencies will transitively affect other modules as well, making your entire infrastructure complex and rigid.
Organizations shouldn't even consider microservices unless they have a system that's too complex to manage as a monolith, according to Kent Beck, creator of test-driven development and extreme programming. Architecture decisions should always revolve around the existing state of your environment, and the modular monolith strategy is no exception.
There are a few scenarios that are a natural fit for modular monoliths, such as:
But, like most architecture patterns, there are also scenarios where the modular monolith may produce diminished returns. These include:
In modular monolith systems, it helps to keep the code layout, access methods, and dependencies associated with each module relatively consistent. Typically, this is accomplished by building guiding conventions around compile-time and build-time processes.
There is plenty of tooling available to help build these required guardrails. SonarQube provides static code analysis highlight and prevents the proliferation of cross dependencies and code complexity over time. Tools like Checkstyle, JDepend and GitHub's CodeAssert are all designed to enforce a layout and access structure at build time using defined rules and configuration.
29 Oct 2020