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.
What is a modular monolith?
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.
Benefits and disadvantages of a modular monolith
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.
What is the code structure of a modular monolith?
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:
- The module presents itself to external applications and services as an API, using protocols such as REST HTTP or GRPC. A reverse proxy, API gateway or other type of messaging broker can then manage these API calls, and do so among multiple modules simultaneously.
- Internal services and applications interact with the module via an abstracted interface component. This interface enables requesting applications to gather the information they need. However, they cannot access the actual implementation and must rely solely on the abstractions. This maintains a proper separation of concerns without compromising application processes.
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.
When do modular monoliths make sense?
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:
- A greenfield implementation, such as a new peer-to-peer lending platform in the early stages of growing into a full suite of financial services.
- A system with a low to moderate scale, such as a movie ticket booking platform that serves several thousand users and handles a few million transactions per week.
- Non-complex business software platforms, such as a notes/documents syncing and management platform for consumer apps.
- Oversized legacy applications, such as an existing large-scale banking platform monolith that needs to split into microservices, but isn't quite ready to separate into completely independent services.
But, like most architecture patterns, there are also scenarios where the modular monolith may produce diminished returns. These include:
- When parts of the platform are being built and managed by multiple independent teams, especially if each team governs its choice of underlying technology and runtimes;
- New, large-scale platforms built in mature environments where service templates or service chassis are available, which significantly reduce the entry cost of microservices; and
- When the scale and volume of business are already consistently high, the IT staff has a solidified understanding of the architecture, and it makes sense to dive straight into microservices.
Enforce consistency through tooling
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.