The strangler fig pattern replaces a legacy system one piece at a time: a routing layer goes in front of the old application, each piece of functionality moves behind it to new code, and the old system keeps running until nothing sends it traffic. The code is rarely the hard part. The shared database and the long middle of the migration are where these projects stall, and both can be planned for from the first week.
What is the strangler fig pattern?
The strangler fig pattern is an incremental way to modernize a legacy application. Instead of replacing the whole system in one cut-over, a team builds new code next to the old system and puts an interception point in front of both, which decides for each request whether the old or the new implementation serves it. Functionality moves to the new side piece by piece, and the old system is retired once nothing depends on it. Microsoft and AWS both describe it as a standard pattern for migrating legacy systems (Azure Architecture Center, AWS Prescriptive Guidance).
Martin Fowler named the pattern after the strangler figs he saw in Australian rainforests. The fig starts in the branches of a host tree and grows down to the ground, until it can stand on its own and the host is no longer needed.
He first called it the strangler application and later changed the name to put the fig back in, which is why both names are in use.
How to start: a routing layer in front of the legacy system
Before anything is rewritten, every request to the old system has to pass through something you control. That facade can be an API gateway, a reverse proxy such as nginx, or, inside a monolith, a routing module that decides which implementation serves a call.
On day one it sends everything to the old application and changes nothing for users.
The facade is what makes the rest of the migration reversible. When a new piece misbehaves in production, routing its traffic back to the old code is a configuration change, not a deployment of the old code, and that option is what lets a team ship the first slice with confidence.
Choosing the first slice to migrate
Teams often want to start with the part of the system that hurts most. It is usually the worst choice for a first slice, because the part that hurts is usually the part that everything else depends on.
A good first slice proves the routing, the deployment pipeline and the monitoring on something that can fail without consequences.
| A good first slice | A poor first slice |
|---|---|
| Mostly reads, few writes | Writes to tables other features also write to |
| Few calls into the rest of the system | Shares transactions with other modules |
| Changes often, so new code pays off quickly | Rarely changes, so nobody notices the improvement |
| Has one owner who can accept the result | Billing, payments or anything with regulatory reporting |
Search, reporting screens and read-only APIs are typical first slices. They exercise every part of the new setup while the old system keeps doing the writes. If the monolith has no clear modules yet, enforce them first: our guide to monolith to microservices migration covers the modular monolith step and how to find the seams.
Strangler fig pattern example with Spring Cloud Gateway
For a web application, the facade can be a small Spring Cloud Gateway service. The configuration below sends /orders/** to a new orders service and everything else to the legacy application. Routes are matched in order, so the catch-all goes last. On Spring Cloud 2025.0 and later the routes sit under spring.cloud.gateway.server.webflux; older versions use spring.cloud.gateway.routes.
spring:
cloud:
gateway:
server:
webflux:
routes:
- id: orders-new
uri: http://orders-service:8080
predicates:
- Path=/orders/**
- id: legacy
uri: http://legacy-app:8080
predicates:
- Path=/**
Shifting traffic gradually with weighted routes
Cutting over in one go is rarely necessary.
The Weight predicate splits traffic between two routes in the same group, here ten percent to the new service and ninety to the old one, so the new code meets real traffic before it has to carry all of it.
Raising the share is a one-line change.
- id: orders-new
uri: http://orders-service:8080
predicates:
- Path=/orders/**
- Weight=orders, 10
- id: orders-legacy
uri: http://legacy-app:8080
predicates:
- Path=/orders/**
- Weight=orders, 90
Branch by abstraction inside a monolith
Inside a monolith, where there is no network hop to intercept, the same idea is called branch by abstraction. The calling code depends on an interface, both the old and the new implementation sit behind it, and a flag decides which one serves the call:
public interface OrderLookup {
Order find(OrderId id);
}
@Bean
OrderLookup orderLookup(LegacyOrderRepository legacy, OrdersClient orders,
@Value("${orders.use-new-service:false}") boolean useNew) {
return useNew ? orders::find : legacy::find;
}
Once the new implementation has carried all the traffic for long enough, the flag, the old implementation and eventually the interface are deleted.
That deletion is the part teams forget to schedule.
Handling the shared database in a strangler migration
Routing requests is straightforward. Most legacy systems, though, keep everything in one database, and the new code needs the same data.
There are four ways to handle it, and the choice matters more than any framework decision.
- Keep the old database as the system of record and let the new code read from it, or from a store kept in sync with it. It is the lowest-risk start, and often enough for read-heavy slices.
- Stream changes into the new store with change data capture, for example Debezium publishing row changes to Kafka. The new service gets its own copy without the old code knowing it exists.
- Avoid dual writes from the application. Writing to both databases in the same request looks simple and breaks the first time one write succeeds and the other fails. There is no transaction across the two, so they drift apart silently.
- Put an anti-corruption layer between the new code and the old data model, so the new service is designed around the business rather than around column names from years ago.
Example: a hotel metasearch engine
Our work on a UK hotel metasearch engine followed the first two ideas. Availability search moved out of the relational database into an in-memory data grid, while the database remained the system of record.
The engine went on to serve 300 million queries a day at 80 percent lower infrastructure cost. The case study has the details.
Keeping a strangler migration from stalling in the long middle
The first slice usually goes well. The trouble starts at slice five or six, when the easy parts are done, the old system still runs everything important, and the team is maintaining two systems at once.
Migrations that stall there leave the company worse off than before: twice the infrastructure and twice the knowledge to keep.
- Delete as you go. The old code for a slice is removed in the same iteration its replacement takes full traffic, not in a clean-up phase at the end.
- Track one number: the share of requests served by new code. It should rise every month; when it stops rising, the migration has stalled.
- Give every slice an end date, agreed before work on it starts.
- Treat the facade as a product. It needs an owner, monitoring and a plan for its own retirement, or it becomes the next legacy component.
When not to use the strangler fig pattern
The pattern suits large systems that have to keep running while their architecture changes. For many systems it is more machinery than the problem needs, and our guide to legacy system modernization compares it with the other options, from keeping a system as it is to replacing it.
- The application can be upgraded in place. A small or mid-sized system on old versions often needs an upgrade, not a new architecture. We moved a fifteen-year-old online store and ERP from Java 17 to Java 25 in about four weeks without splitting it (see the case study). With the Java support deadlines arriving between 2029 and 2033, that is the more common job, and our guide to application modernization shows how to decide across a whole portfolio.
- The data model is the problem. If the tables themselves are wrong, moving code around them changes little. The data needs its own migration plan first.
- The calls cannot be intercepted. Nightly batch jobs, shared files and direct database access by other systems bypass any facade, and each needs a separate plan.
- Nobody can run two systems for a year. Without a team that can operate the old and the new side by side, a planned replacement with a fixed cut-over date can be the safer choice.
If you are deciding whether your system needs a strangler migration or a straight upgrade, that is what the first step of our Application Modernization Sprint answers: a written view of the options, the order of work and a ballpark estimate, within a week of a thirty-minute call.