Monoliths and microservices are two basic approaches to building applications. Some people consider them to be legacy and modern, respectively. But this is not quite right. Choosing between them is a very complex question, with both having their pros and cons.
Most new applications do not need to be microservices from the very beginning. It is faster, easier, and cheaper to start as a monolith and switch to microservices later if you find it beneficial.
Over time, as monolith applications become less and less maintainable, some teams decide that the only way to solve the problem is to start refactoring by breaking their application into microservices. Other teams make this decision just because "microservices are cool." This process takes a lot of time and sometimes brings even more maintenance overhead. Before going into this, it's crucial to carefully consider all the pros and cons and ensure you've reached your current monolith architecture limits. And remember, it is easier to break than to build.
In this article, we are not going to compare monoliths to microservices. Instead, we will discuss considerations, patterns, and principles that will help you:
The first thing you should do is look at your project structure. If you don't have modules, I strongly recommend you consider them. They don't make you change your architecture to microservices (which is good because we want to avoid all corresponding maintenance) but take many advantages from them.
Depending on your project build automation tool (e.g., maven, gradle, or other), you can split your project into separate, independent modules. Some modules may be common to others, while others may encapsulate specific domain areas or be feature-specific, bringing independent functionality to your application.
It will give you the following benefits:
As you see, the modular monolith is the way to get the best from both worlds. It is like running independent microservices inside a single monolith but avoiding collateral microservices overhead. One of the limitations you may have – is not being able to scale different modules independently. You will have as many monolith instances as required by the most loaded module, which may lead to excessive resource consumption. The other drawback is the limitations of using different technologies. For example, you can not mix Java, Golang, and Python in a modular monolith, so you are forced to stick with one technology (which, usually, is not an issue).
Think of the Strangler Fig pattern. It takes its name from a tree that wraps around and eventually replaces its host. Similarly, the pattern suggests you replace parts of a legacy application with new services one by one, modernizing an old system without causing significant disruptions.
Instead of attempting a risky, all-at-once overhaul, you update the system piece by piece. This method is beneficial when dealing with large, complex monoliths that are too unwieldy to replace in a single effort. By adopting the Strangler Fig pattern, teams can slowly phase out the old system while fostering the growth of the new one, minimizing risks, and ensuring continuous delivery of value.
To implement the Strangler Fig pattern, you need to follow three steps:
Taking these three steps, you will gradually break a monolith into microservices.
The key benefits of using the Strangler Fig pattern are:
When applying the Strangler Fig pattern, you should plan the migration strategy carefully. It includes identifying which components to replace first, ensuring proper integration between old and new parts, and maintaining consistent data models. Teams should also establish clear communication channels and monitoring systems to track the progress and impact of each replacement.
As we discussed earlier, transitioning to microservices brings benefits. However, it also makes many other things more difficult and expensive.
For example, QA and Dev teams might face a situation where they can no longer test on local machines because the ability to run multiple microservices locally is limited. Or your logs may become less insightful because, instead of one service processing a single flow, multiple services process it now. As a result, to view the complete log sequence, you need to implement proper instrumentation and tracing.
Let's discuss a few crucial aspects you should consider and maybe plan before your microservices transformation starts.
Monolith and microservices have different minimal infrastructure requirements.
When running a monolith application, you can usually maintain a simpler infrastructure. Options like virtual machines or PaaS solutions (such as AWS EC2) will suffice. Also, you can handle much of the scaling, configuration, upgrades, and monitoring manually or with simple tools. As a result, you can avoid complex infrastructure setups and rely on developers or support engineers without requiring extensive DevOps expertise.
However, adopting a microservices architecture changes this dynamic. As the number of services grows, a manual, hands-on approach quickly becomes impractical. To effectively orchestrate, scale, and manage multiple services, you'll need specialized platforms like Kubernetes or, at least, a managed container service, introducing a new layer of complexity and operational demands.
Maintaining a monolith application is relatively straightforward. If a CVE arises, you update the affected dependency in one place. Need to standardize external service communication? Implement a single wrapper and reuse it throughout the codebase.
In a microservices environment, these simple tasks become much more complex. What was previously trivial now involves coordinating changes across multiple independent services, each with its lifecycle and dependencies. The added complexity increases costs in both time and resources. And the situation worsens when you have many teams and many different services.
Therefore, many organizations introduce a dedicated platform layer, typically managed by a platform team. The goal is to create a foundation that all microservices can inherit: managing common dependencies, implementing standardized patterns, and providing ready-made wrappers. By unifying these elements at the platform level, you significantly simplify maintenance and foster consistency across the entire system.
Breaking a monolith into microservices is a significant architectural transformation that requires careful consideration and planning.
In this article, we've discussed steps you can take to prepare for it and go through the process smoothly. Sticking to the Strangler Fig pattern will provide you with the incremental process and ensure system stability throughout the transformation. Also, the modular monolith can not only be a helpful preparation step but also can bring benefits that may prompt you to reconsider your microservice transformation decision and avoid corresponding operational complexity.