Transition to Microservices: Strangler Fig Pattern
Microservices is a common approach to decompose a large application into smaller, self-contained, and interconnected applications.
The migration from an existing standalone application, or monolith, to microservices is an architectural challenge that might take months or years. For this reason, it is important to look at best practices and patterns to tackle such transition in the most effective way.
One of the most important and used methodology to decompose an application is the Strangler Fig pattern. The Strangler Fig pattern, identified by Martin Fowler [1], takes the name from a fig commonly found in Asia. The fig grows on top of an existing tree, trying to push its roots down to reach the ground. Once the fig takes root, it gradually grows around the host tree, stealing the sunlight and the nutrients in the ground, eventually killing the host. When the host tree rotes away, an empty structure remains, which is the strangler fig. The figure below shows this process with the host tree depicted in gray and the fig in green.
Strangler Fig lifecycle [2]
This is a strangler fig in Hualien:
Strangler Fig in Hualien
In the context of software, the new system (fig) will be supported initially by the monolith (host tree) and then it will gradually replace it. The key is to have an incremental migration, able to stop and roll back at any moment.
Benefits of the Strangler Fig pattern:
- Incremental migration
- No (or limited) changes to the existing system
- Easy rollback at any moment
This patterns consists of three steps:
1. Identify the feature to extract
2. Implement the feature as a standalone application (microservice)
3. Reroute the requests to the microservice
The steps are highlighted in the figure below.
Feature extraction using the Strangler Fig pattern
Identifying the functionality
To identify the functionality to extract is important to have the architecture diagram of the existing system and to highlight the dependencies between the modules.
To facilitate the decomposition, a module (or component) should have a clear bounded context. For example, consider the architecture diagram below:
Architecture diagram of a traditional application
The module Invoicing has no inbound dependencies from other modules, making it easier to extract. The client calls to the Invoicing module can be redirected with a reverse proxy (more details on the last section of the article).
The easiest functionality to extract doesn’t necessarily mean that is the best candidate. It’s also important to consider what are the benefits of such decomposition.
Regarding the data, there are no guarantees on how it is structured, therefore the database decomposition has to be taken into account. However, the database decomposition is a longer and more tedious process that can typically be postponed to a later stage. This is also because a main aspect of the migration to microservices is to gain “quick wins”, by showing the results incrementally and quickly.
Implementing the microservice
The simplest scenario is when a functionality in the existing application has no dependencies to other modules. In that case, no changes to the monolith are required and it can be treated as black box. In the figure below, the module Inventory Management is extracted into a microservice.
Inventory Management module extraction
Until the requests are rerouted, the microservice is not in use, so it can be safely deployed to production. Additionally, the migration can be rolled back in any moment.
If the extracted module has outbound dependencies, the monolithic application can expose an API to let the microservice use such dependency. For example, the Payroll feature depicted in the figure below, has an outbound dependency to User Notifications module:
Payroll module extraction
If many functionalities in the monolith use the extracted functionality, this pattern does not work well. Extracting the User Notification module would require a substantial effort since it’s used by Payroll and Invoicing.
Rerouting the calls
If the application is interfaced with HTTP protocol, the inbound requests can be rerouted using a reverse proxy.
In such case, these steps can be followed:
1. Insert the proxy between upstream requests and the monolith
2. Deploy the microservice
3. Reconfigure the proxy to redirect the calls to the microservice
Regarding step 3, it is important to don’t remove the existing functionality in the monolith, in case unexpected errors occur and a rollback is needed.
One of the easiest ways to route HTTP calls is based on the URL. For example:
- If URL matches "https://my.app/invoice/*": route the request to the Invoice microservice
- If URL matches "https://my.app/*": route the request to the monolith
Such routing rules are commonly supported by reverse proxies, like NGINX.
Additionally, the redirection can be based on specific parameters in the body of the request, but for more complex routing mechanisms the capabilities of the specific reverse proxy needs to be considered.
If the existing application uses a different protocol, e.g. SOAP, and the microservice exposes a traditional HTTP REST interface, the reverse proxy can translate the SOAP requests to HTTP requests.
As a final advice, it is better to keep the reverse proxy simple, to avoid implementing another complex system that needs to be maintained and updated frequently.
References:
[1] https://martinfowler.com/bliki/StranglerFigApplication.html
[2] http://daplantnerd79.blogspot.com/2017/10/
[3] Monolith to Microservices: Evolutionary Patterns to Transform Your Monolith (Sam Newman)