In recent years we have seen a significant increase in apps built using a microservices architecture. The main reason we select this approach is to have small teams working in isolation without having them trip over each other. Yet, this is an organizational problem, not a technical one. We can also build each service in different technology and scale independently.
With the microservice approach, we have a few disadvantages, too. The system is becoming complex to maintain and diagnose issues (logging and tracing). And this is very important when dealing with microservices. Yet, we also saw something called “microservice bloatware,” even on Twitter:
But also many more examples where the microservices approach fails, if you’re not solving a problem at Netflix size, you probably don’t need microservices.
On the other hand, we have monoliths with a lot of lousy wording about them. But building monoliths doesn’t mean something better per se. In the last years, we often saw the identification of monolith with a big ball of mud architecture or purely building legacy code, which doesn't mean to be. Yes, monoliths cannot scale or release independent pieces of the system separately, but those are mainly the most significant downsides. Still, you can create tremendous and high-quality code inside. Monolith brings us much less complexity, reduced network calls, more detailed logging, etc. Most subsystems of an entire application or system are stored in the monolith container. It is called self-contained since every system component is housed in a single container.
We can have architected monoliths that will fulfill all our use cases and requested architecture attributes in the system without having to deal with the complexity of microservice architecture. The best example is Shopify, with over 3 million lines of code, one of the giant monoliths in the world. Instead of rewriting its entire monolith as microservices, Shopify chose modularization as the solution, while they served 1.27 million requests per second during Black Friday. But there are also more examples of monoliths, such as StackOverflow, Basecamp, or Istio. Also, recently we saw that one team in Amazon (Prime Video) abandoned microservice architecture in favor of monolith.
We want to have separate modules and work on them but maintain simplicity to build a modular monolith. An adequately produced modular monolith can be a good step that can be more or less transformed into a microservice solution tomorrow if needed. So, the recommended path is Monolith > apps > services > microservices.
When we want to build a modular monolith, it is crucial to divide the system into manageable modules before assembling them into a monolith for deployment. As all communication between the modules might result in a cross-network call if you decide to break it into services in the future, high cohesion, and low coupling are crucial in this situation. This means that all inter-module communication must be abstracted, asynchronous, or based on messaging for the modules to handle calls that travel across the network in the future.
How can we put in place such a concept? First, we create separate modules; each has its architecture, and those modules are pulled together into a single API gateway. This allows us to deploy the whole system as a monolith, but it will enable us to pull out separate modules into services if needed in the future.
What is Distributed Monolith?
There are three types of monoliths:
These are the most common monolith types, where everything is bundled together. Usually, we have some user interface, business logic, and data access layers in a single tier and deployment. There are no clear boundaries between domains, and the code will have shared libraries.
With modular monoliths, we have defined precise functional slices and dependencies, meaning we can have each module independent of the other. Yet, there will still be a single deployment unit and a single database. This is what we want to achieve.
Here we have one modern monolith variant, where our system is deployed like microservice architecture, but it is built with monolith principles in mind. We usually get to this point while attempting to create a microservice architecture without considering some architectural and process changes needed. For example, we know we have a distributed monolith when some services cannot be deployed separately, are chatty, and cannot scale and share the same data source.
This is an obvious example of an anti-pattern. These systems are built like monoliths but deployed similarly to microservices. Here we have the worst of both worlds, tightly coupled services with the complexity of microservices. Unfortunately, we usually get here by developing microservice architecture wrongly.
Monolith Decomposition Strategy
If we are stuck with traditional or distributed monoliths, we need to do some monolith decomposition. There are a few approaches that can help here:
Strangler Fig Pattern
Strangler Fig Pattern (Coined by Martin Fowler) comes from a collection of plants that grow by "strangling" their hosts. This pattern enables to replacement of specific functionality with new services. Here we create a façade that intercepts requests going to the monolith and routes these requests to the monolith or new services. And we gradually migrate old functionality to new services, yet consumers always hit the façade.
Here you can also use Domain-Driven Design (DDD) to incrementally refactor the application into more minor services, where you first find ubiquitous languages (common vocabulary) between all stakeholders, then identify relevant modules to apply this vocabulary to them and define domain models of the monolithic application. In the last step, you define bounded contexts for the models, which are boundaries within a domain.
Branch by abstraction
With this approach, we create an abstraction layer over our original component so that we can replace it step by step. Client requests are directed to this layer, allowing us to change everything behind it. When we finish with changes, the client will hit only the new component. With this pattern, we can coexist two implementations of the same functionality, the true Liskov substitution principle. Although, like the Strangler Fig pattern, with this one, we are working on a bit lower level of abstraction, where our focus is more on components than the systems.
If you're interested in this approach, I recommend the following book: “Monolith to Microservices,” by Sam Newman.
Modular Monolith: A Primer, Kamil Grzybek.
Build the modular monolith first, Chris Klug.
The Majestic Monolith, David Heinemeier Hansson.
Strangler Fig pattern, Martin Fowler.
eShopOnWeb by Microsoft - Monolithic Web application built using ASP.NET.
eShopOnContainers by Microsoft - Microservices Web Application, which is more complex, scalable, and resilient.
Microservice architecture, Chris Richardson.
🎁 If you are interested in being a sponsor of one of the following issues and supporting my work while enabling this newsletter to be accessible to readers, check out the opportunity for Sponsorship Tech World With Milan newsletter.
Thanks for reading Tech World With Milan Newsletter! Subscribe for free to receive new posts and support my work.
Great post, one comment: structure your monolith on business modules, not technical ones (as controllers, services, daos...). This business modules couples with another ones and you can separate as independent: modules, packages, components, libraries, jars, microservices...