It's arguable that distributed monoliths aren't always a bad thing, and those who currently run a distributed monolith may not notice any significant problems. However, this setup will not meet the goals desired by those pursuing something like cloud-native microservices: to create truly independent units of functionality designed to maximize a development team's ability to scale, redeploy and incrementally reconfigure applications.
In this article, we look at how the distributed monolith is defined, how it affects architecture management, and why taking a different perspective might reveal the actual value of this oft-ridiculed architecture approach.
What is a distributed monolith?
Before defining a distributed monolith, it might help to first define what we mean by monolithic. In a monolithic application, functions are woven directly into the individual components that make up the application, rather than separated into independent units of functional logic. Few would argue that taking the functions that span a monolithic application and breaking them into independent entities is necessarily a bad thing.
When a monolithic application is componentized, it's possible to spread a feature across multiple components. That means that those components are now tightly coupled, and that, in turn, limits how well they exploit the benefits of cloud-native microservices. A popular view of a componentized, distributed application looks like the figure here, which shows workflows or event flows linking the components.
This diagram could potentially represent two contrasting design styles. Some architects, for example, could use this to illustrate a cloud-native microservices application. Others, however, would call it a perfect example of the functional relationships within a distributed monolith. The difference between these two perspectives lies not in the component or workflow design, but in how the components actually map to specific features and the functional logic behind them.
If scaling and resilience are not your main concerns, then creating containerized services separated by function would foster an optimum architectural structure for your applications. But, if those services are designed such that they share logic with multiple other services and components, you've likely created a distributed monolith.
The signs of a distributed monolith
Many enterprises end up with distributed monoliths by accident, most often because the paradigm of functional unity that characterizes microservices design gets ignored during the transition.
During a true microservices migration, it's critical to ensure that application changes do not simultaneously distribute new functionality across multiple microservices -- a setup that essentially reflects the structure of a service-oriented architecture (SOA) model. That can only be prevented by a review of the proposed change model associated with all application maintenance. Many organizations simply overlook the importance of this kind of review, and others fail to enforce functional separation even if they see the issue.
How do you know if you've turned a good microservice into the bad kind of distributed monolith? The following conditions are a good indication that you have one:
- A small change to application logic requires concurrent changes to many other components.
- Multiple microservices and components access the same database.
- The number of parameters passed to multiple microservices continually grows over time.
- Multiple microservices and components share common, boilerplate code.
- Microservices retain compounding amounts of logic with each successive change to a system.
Taming the distributed monolith
If you find you have a distributed monolith, there are only two options available: Live with it, or start over and reinforce the proper componentization. The former choice isn't always wrong; if your application isn't changed often and doesn't require cloudlike scalability or resiliency, it may be hard to justify the time and cost required to initiate a do-over. However, if you truly need to break out of a distributed monolith implementation, don't fall into the trap of thinking that simple changes will fix the problem.
The componentization issues in play stem from improper division of features, which means you need to go back and carefully break up individual functions into reasonably partitioned modules. Sometimes, it's possible to do that by retracing the steps that put you in a distributed monolith. Depending on how many changes have occurred in the meantime, however, it might be more straightforward to simply go back to your original monolith and start from square one.
Start by grouping features. Document and catalog all the functional logic associated with each one -- already a common practice in SOA design. Once you've identified your feature groupings, look at each group, and decide whether there is a benefit to parsing it out into an independent unit of functionality, such as whether doing so will significantly improve scalability or resilience.
Once you have your fully decomposed microservices structure, map the workflow among the components, and take note of anything that will require significant changes or updates to the existing application infrastructure. For example, if you want scalability, you need to think about adding load-balancing and discovery capabilities. In this process, you'll likely find some type of middleware mechanism, such as service mesh, that can assist in standardizing the process of mapping features to microservices.
Remember: Traditional monoliths aren't always bad, and fully meshed, cloud-native microservices aren't always good. But, if you think you made a serious mistake in your attempt at breaking up a monolith, the thing to do is go back, make better choices and directly remedy that initial error in componentization.