Microservices architecture has many advantages compared to monolithic architecture. Hence, many organizations start building new applications with microservices or convert existing monoliths to microservices.
However, transforming a large-scale monolithic application to microservices from scratch is a challenging task. It requires significant architecture and development effort and poses a higher risk to the organization. Nevertheless, some well-established patterns and practices may help to reduce these risks.
Let's look at how we can use the Strangler pattern to transform a monolith into microservices.
According to Statista, over 80% of organizations worldwide use microservices, and 17.5% plan migrating their existing architectures to microservices. But, writing an existing monolithic application from scratch with microservices is a challenging task. As mentioned, it involves severe risks and requires significant developer effort.
For example, here are some of the challenges you might face when writing an application from scratch:
- Time-consuming - Rewriting an application can take a significant time. Especially if your application is large-scale, it can take up to years. Also, there can be unexpected issues that can take a considerable amount of time to resolve.
- Can't develop new features - The rewriting process requires a colossal developer effort. You might have to divert the whole development team to the migration task, which will slow down the new feature development.
- Uncertainty - You must wait to use the new system until the development process completes. Then only you can ensure that all the existing functionalities are working as expected.
The Strangler Pattern minimizes these risks and uncertainties while providing a way to gradually refactor monolithic applications to microservices rather than writing from scratch.
Furthermore, organizations don't have to pause new feature development or wait long to see a working application. Instead, they can choose components based on priority and refactor them to microservices while keeping monolith and microservices as a single application.
Now we are beginning to understand the need for the Strangler Pattern. So, let's get into more detail on the Strangler Pattern and how it works.
Strangler Pattern is a software design pattern used to refactor monolithic applications to microservices gradually. It helps developers to replace parts of the monolith with new and improved components while maintaining the same functionality.
The Strangler Pattern uses a wrapper to integrate the microservices with the monolith. This wrapper is an integral part of this design pattern since it bridges the monolith and the microservices, directing incoming requests to the appropriate component for processing. Furthermore, it acts as a fail-safe, allowing the organization to roll back to the monolith if there are any issues with the new microservice.
To get a better understanding, let's discuss how Strangler Patterns work using a real-world scenario.
Are you enjoying this article about the Strangler Pattern and how you can migrate from a monolith to microservices? If so, we invite you to check out Amplication, the low-code development platform that can help you make that migration with ease. And, if you appreciate our mission to simplify app development for everyone, please consider showing your support by giving our repo on GitHub a 🌟. In return we'll send you an imaginary star which you can stick on anything! Thank you for your support.
Refactoring a monolith into microservices with the Strangler Pattern consists of 3 main steps: Transform, Coexist, and Eliminate.
- Transform: You need to start by identifying the main components of the monolithic application. This step involves identifying the boundaries between the existing application and the new components being developed.
- Coexist: Then, build a wrapper around the monolith to allow the new components to coexist with the existing application.
- Eliminate: Finally, eliminate the monolith by replacing parts with new components. However, you must ensure that each microservice works as expected before integrating it into the system.
For example, consider an e-commerce application with a monolithic architecture. The application will include user registration, product search, shopping cart, payment handling, inventory management, and more.
To give you a better understanding, I will divide the migration process into five steps.
As the first step, you need to individually identify the monolith components with their capabilities and limitations.
Then, it would be best to decide on the first microservice you will migrate. There are several factors to consider when ordering the components for refactoring, and I have explained them in detail in the next section.
In this example, I have decided to use product search functionality as the first component to migrate since it is not in the application's critical path.
Also, it is crucial to identify the dependencies between the selected component and others. In this example, the product search component is directly connected with inventory and shipping cart components.
As the second step, you must start creating the new microservice and move all the relevant business logic to the new microservice. Then, it would help if you created an API for the microservice, which will act as the wrapper to handle communications between the monolith and the microservice.
After implementing the microservice, you need to run both the monolith's component and the microservice in parallel to verify the functionality.
When you create a new microservice, it is necessary to create a separate database. In addition, since we are maintaining both monolith and microservice in parallel for a time, we also need to keep the primary and microservices databases in sync.
For that, we can use a technique like read-through-write-through caching. In this example, we will use the microservice database as a cache until we remove the product search component from the monolith. Then, when a user searches for a product, we look in the microservices' database. We can fetch the data from the primary database if it is not there.
Initially, routing a small percentage of traffic to the new search service is advised to reduce the blast radius of the new microservice. For that, you can use a load balancer.
Once you verify the functionality of the new microservice, you can remove the component in the monolith and route all the traffic to the microservice. Then, you must repeat these steps for each component and gradually convert the monolith into microservices.
Another major issue developers face when using the Strangler Pattern is the component order for refactoring. Although no hard rules are defined for the selection process, it is essential to sort out what components should be migrated first to avoid unexpected delays.
Here are some factors you need to consider when deciding the order of the components:
- Consider dependencies: It is good to start with components with few dependencies, as these are likely to be easier to refactor.
- Start with low-risk components: Starting with low-risk components will minimize the impact on the system if something goes wrong in the early stages. Also, it will help you to gain experience and build confidence in the process.
- Business needs: If there is a high-demand component and you need to scale it as soon as possible, you should start with that component to facilitate the business requirement.
- Components with frequent changes: If components require frequent updates and deployments, refactoring them as microservices will allow you to manage separate deployment pipelines for those services.
- User experience: Start with the components with the most negligible impact on end-users. It reduces disruptions and helps to maintain a good user experience during the transition.
- Integrations: Refactor components with minimal integration with other systems first.
Apart from the above, there are many other factors you can consider. However, ultimately you need to consider the most prominent factors for your project and order the components for the refactoring process.
Unlike other design patterns, the Strangler Pattern has no language-specific libraries to help developers implement it. Instead, developers must use technologies, frameworks, and best practices to implement the Strangler Pattern.
Here are some of the most used approaches in implementing the Strangler Pattern:
- Using ready-made platforms: Instead of building the infrastructure and platform architecture for microservices from the ground up, you can consider utilizing ready-made platforms that mostly do the heavy lifting. E.g., Amplication, Strapi, AppWrite
- Using serverless: You can use AWS Lambda or Google Cloud Functions to implement independent functions triggered by specific events and eventually replace the parts of the monolith with them.
- API gateways: An API gateway can be the wrapper when implementing the Strangler Pattern. It provides a unified interface and can be configured to redirect requests to the appropriate component. Amazon API Gateway, Kong, and Tyk are some popular API gateways you can use.
- Reverse proxies: A reverse proxy like Nginx can also be used as the wrapper in the Strangler Pattern.
- Routing and load balancing: Routing and load balancing technologies can redirect traffic to the appropriate components. DNS-based routing and software-defined load balancers are popular options you can use.
- Service discovery: You can get the help of the service discovery pattern to find the locations of new microservices.
- service mesh: You can use technologies like Istio or Linkerd to manage the communication between the new components.
These are only a subset of tools and technologies you can use to implement the Strangler Pattern. But make sure to only select a limited number of technologies from them based on your requirement. Otherwise, you will end up over-engineering the system.
- Incremental migration: Allows for an incremental migration from a monolith to microservices and reduces the risk associated with the migration process.
- Reduced downtime: Ensures the system remains operational throughout the migration.
- Improved resilience: Improve the system's resilience by ensuring that the monolith and microservices coexist and work together seamlessly.
- Increased flexibility: Allow the organization to choose the best technology for each part of the system, rather than being forced to use a single technology for the entire system.
- Better maintainability: Breaking down the monolith into microservices makes it easier to maintain the system over time.
- Modularizing complexity: Breaking a monolith into components is difficult when the functionalities are tightly coupled.
- Data compatibility: Sometimes, you may need to perform a data migration or transformation when the monolith and the microservices use different data formats.
- Testing effort: Extensive testing is required to ensure the new microservices work as expected.
- Skill requirements: Using the Strangler Pattern requires a high level of technical skill. Implementing the pattern in organizations that need more technical expertise can make it challenging.
This article discussed how the Strangler Pattern simplifies the conversion of monoliths to microservices while highlighting the advantages and challenges of the process.
However, converting a monolith with hundreds of components is challenging, even with the Strangler Pattern. You need to design the underlying architecture for microservices and work on creating each service from scratch.
One solution is to create a blueprint for each microservice and develop the tools to generate it. You can even consider developing a domain-driven language to standardize and reuse best practices in code and configuration. However, it also adds complexity and cost to your architecture, where you need to manage and evolve these tools in the long run.
Amplication can manage all the underlying complexities while simplifying the creation of microservices.
Amplication supports microservices through the project hierarchy. A project groups together multiple resources used and created by Amplication, enabling support for various use cases. This simplifies the creation of connected services and makes syncing with GitHub across multiple Services much easier.
You can find a getting started guide here.