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:
- get the best of your current monolith and potentially prepare it for breaking into microservices;
- provide seamless migration from monolith to microservices;
- understand what else may change with microservices migration.
Modular monolith
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:
- Decoupled parts of your application.
- Features can be developed independently, minimizing conflicts.
- It’s much easier to onboard new people since they don’t need to dive deep into the whole project; instead, they can start from isolated parts.
- Improved testing, because now you can test each module separately.
- If you need to migrate to microservices eventually, you have a very strong basis for it.
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).
The Strangler Fig pattern
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:
- Transform. Here, you identify which part to extract from the monolith and implement it as a separate microservice. Rewrite this part using modern technologies and address the problems of the previous implementation. Take your chance to do it better.
- Coexist. When a new microservice is ready for production, you run it in your infrastructure, keeping the legacy implementation. You distribute traffic in some proportion, gradually moving more and more traffic to the new implementation, but you always have your previous version as a rollback.
- Eliminate. After some time, when you believe that your new microservice is reliable enough, move all traffic into the new microservice and remove the legacy.
Taking these three steps, you will gradually break a monolith into microservices.
The key benefits of using the Strangler Fig pattern are:
- Gradual Migration. Rather than breaking bad and starting a timely, overwhelming, and risky complete system overhaul, the pattern allows you to transition step by step. You can slowly update your system and synchronize this transformation with feature development plans. For example, you can find a part of the monolith that some feature development will seriously affect and choose it as a candidate for microservice migration.
- Continuous Delivery. This benefit comes from the previous statement. The modernization process does not block your team from continuously delivering new features. While one team develops new features, another refactors independent modules.
- Risk Mitigation. The Strangler Fig pattern helps the team focus on modernizing specific parts of the system. This focus enables the team to pay closer attention to details in specific areas, find potential problems earlier, and minimize the overall impact on the application.
Issues and considerations
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.
Microservices migration repercussions
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.
Deployment and infrastructure
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.
Platform layer
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.
Conclusion
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.