A full rewrite of a legacy system looks clean on a whiteboard. Martin Fowler lists three reasons these projects struggle: teams find it hard to pin down the details of existing behavior, users can't wait for new features until the replacement ships, and much of the existing behavior turns out to be unwanted. And as the rewrite drags on, business needs keep changing, so the target the new system has to hit keeps moving.
The Strangler Fig Pattern splits that risk into slices. Fowler took the name from strangler figs he saw in the Queensland rain forests in 2001. The plant germinates in a nook of a host tree, draws nutrients from it, and can end up self-sustaining; the host tree may die. In software, teams build new capabilities on top of the legacy code base but separate from it, then move behavior across one piece at a time. The old system shuts down once nothing is left to move. Each slice has to handle routing, data, and a way back.
How the Strangler Fig Pattern works
A facade (a proxy) sits between clients and the back-end systems. Microsoft's Azure Architecture Center describes four phases:
- The team places the facade between the client app, the legacy system, and the new system. At first, the facade sends most requests to the legacy system.
- With each iteration, the team implements another capability in the new system and the facade shifts the matching requests to it.
- Once all functionality has moved and nothing depends on the legacy system, the team decommissions it. The facade now routes every request to the new system.
- The team removes the facade and points clients at the new system directly.
Clients keep using the same interface throughout and don't see the migration. Microsoft notes one alternative for the last phase: the facade can stay in place as an adapter for legacy clients while the team updates the core system for newer clients.
When it fits and when it doesn't
The table below brings together the conditions in the Microsoft and AWS documentation.
| Good fit |
Might not fit |
| Replacing a large system or key components in one step carries real risk |
Requests to the back end can't be intercepted |
| The legacy system can keep running for an extended period |
No access to the legacy source code and no other integration point you can change, such as the database |
| The business can't wait for the transformation to finish before getting new features |
The system is small and a full replacement is simple |
| End users must see minimal disruption |
The original system has to be decommissioned quickly |
AWS treats source access as a requirement, because the team has to make minor code changes in the monolith and add an anti-corruption layer inside it to route calls to the new services; without code access, AWS says, those calls can't be intercepted. For components you can't modify, there are other options. For database-centric applications with logic in stored procedures, Microsoft points out that the database itself can send messages to new services, or a Change Data Capture (CDC) process can read the transaction log.
Step 1: define the outcome and find the seams
In "Patterns of Legacy Displacement," Ian Cartwright, Rob Horn, and James Lewis of Thoughtworks break the work into four activities: understand the outcomes you want to achieve, decide how to break the problem into smaller parts, deliver the parts successfully, and change the organization so it can keep doing this. If the team skips the first activity, it stays unclear which outcome comes first: lowering the cost of change, improving a business process, retiring an old system, or another goal. The Thoughtworks authors note that most efforts pursue several of these at once and that the priority has to be set before the work starts. Without a priority, nothing is left to decide the order of the slices.
The second activity depends on seams, the insertion points that let you split a system into manageable pieces. Fowler writes that a well-designed system already has them; legacy systems rarely do, so working out how to split the system and adding those seams takes significant work. AWS adds a warning and a recommendation. Decomposing too early, while the domain is still unclear, can get expensive and can draw service boundaries in the wrong place; AWS recommends Domain-Driven Design and Event Storming to find them. As good candidates, AWS points to services that own their data access object, keep related business logic together, and have few dependencies.
Four questions help pick the first slice:
- Can you tell this capability's requests apart by URL path, message type, or table?
- Which tables does its data share with other domains?
- Which parts of the legacy system will still call it after it moves?
- If the new version misbehaves, how long does it take to send traffic back?
AWS also recommends building new features as new services instead of adding them to the monolith, while the team keeps fixing bugs in the monolith. That limits how much the legacy system grows during the migration.
Step 2: put the facade in place
The facade can be an API gateway or a reverse proxy. The first version passes every request to the legacy system. When the first slice is ready, only that slice's path goes to the new service. AWS routes by API URL in its example. This NGINX configuration shows the same idea:
upstream legacy_app { server legacy.internal:8080; }
upstream user_service { server users.internal:9000; }
server {
listen 80;
# Migrated slice: user operations go to the new service
location /api/users/ {
proxy_pass http://user_service;
}
location = /api/users {
proxy_pass http://user_service;
}
# Everything not yet migrated stays on the legacy system
location / {
proxy_pass http://legacy_app;
}
}
Among prefix locations, NGINX selects the longest matching prefix, and this example has no regex locations that could override that choice. Requests starting with /api/users/ reach the new service and the rest reach the legacy system. Because that prefix ends with a slash and the request is handled by proxy_pass, NGINX answers a request for /api/users (no trailing slash) with a 301 redirect to /api/users/. To avoid that redirect, the example adds an exact-match location (location =), as the NGINX documentation suggests, so the request goes straight to the new service. Each new slice means adding its location blocks and reloading the configuration; removing them and reloading sends traffic back. Rolling back data is a separate problem, covered in Step 4.
The facade brings its own risks. Microsoft and AWS both warn that it must not become a single point of failure or a performance bottleneck. Microsoft also says the facade has to keep up with the migration. In practice, every migrated slice needs a matching routing rule.
Tooling changes too. AWS built Migration Hub Refactor Spaces for this job, but the service stopped accepting new customers on November 7, 2025; AWS now points to AWS Transform for similar capabilities. Keeping the routing rules on a gateway or proxy your team already runs lowers the risk of a forced tool change in the middle of a migration.
Calls that bypass the facade
Not every call arrives over HTTP from outside. Calls inside the monolith, and integrations that go through message queues or the database, bypass the facade. Two techniques cover these cases:
- Branch by Abstraction: In Fowler's 2014 write-up, the team puts an abstraction layer in front of the existing module and moves callers onto it. They build the new implementation behind the same abstraction, switch callers over in stages, and delete the old code at the end. The system stays releasable the whole time.
- Event Interception: In the Thoughtworks description, you intercept updates to system state and route some of them to the new component. The interception point can be the messaging infrastructure (Wire Tap, Message Router, Content-Based Router), an API gateway, a script added to a web application's template, or pre-commit triggers in the database. The authors aim to keep changes to legacy components to a minimum and also cover post-commit triggers and CDC as alternatives.
Step 3: separate the two models with an Anti-Corruption Layer
During the migration both systems keep calling each other. The new system reaches back to the legacy system for functionality that hasn't moved, and legacy components call functionality that now lives in the new system. Microsoft recommends an Anti-Corruption Layer (ACL) for these calls. Eric Evans described the pattern in Domain-Driven Design: the layer translates requests between two subsystems, so the new model doesn't have to adopt legacy schemas or the semantics of obsolete APIs.
In the AWS example, the team moves the user service to a microservice, but the cart service inside the monolith still calls it at its old location. The team implements the ACL inside the monolith as a service-specific class such as UserServiceFacade or UserServiceAdapter, which converts the call to the new interface and forwards it. The cart service doesn't need to know about the move. When the cart service migrates too, it calls the new service directly and that ACL path goes away.
Microsoft's ACL page raises these design points:
- The ACL adds latency and one more service to run, so plan how it scales.
- Keep it focused on translation logic; business rules and orchestration don't belong in the ACL.
- It sits between systems with different trust levels, so input validation and sanitization fit at this boundary.
- Plan correlation IDs and structured logging so you can diagnose translation failures.
- Monitor transaction and data consistency.
The Thoughtworks Legacy Mimic pattern handles the same problem during the transition. A service-providing mimic hides the new implementation behind a legacy interface, so old components don't notice the change. A service-consuming mimic talks to unreplaced legacy systems through their existing interfaces. In their example, the authors mark both mimics as transitional.
Synchronous calls between migrated services add new failure paths. AWS notes that timeouts can tie up connections or thread pools, and recommends a circuit breaker for calls that are likely to keep failing for a long time. Our Enterprise Software Architecture post covers circuit breakers, retries, and fallbacks in a broader context.
Step 4: move from a shared database to domain databases
Most legacy systems run on a central database that serves several domains. Rolling back a traffic route usually means changing one rule; rolling back after data ownership has moved costs far more. Microsoft's database example splits this into three phases:
- New service, old database: the new service starts handling requests for its domain but still reads and writes the domain's tables in the monolithic database. The legacy system keeps serving every other domain.
- Separate domain database with sync: an ETL process copies the domain tables and historical data into a new domain database, then CDC syncs changes from the monolithic database. CDC has to pick up every change made after the data the ETL copied; CDC tools such as Debezium first take an initial snapshot and then stream changes from the log position recorded with that snapshot. The team validates consistency between the two before cutover.
- Cutover: after validation, the domain database becomes the system of record. The team removes the matching tables, stored procedures, and dependencies from the monolith.
The rollback window narrows in that order. According to the same example, you can still return to the monolithic database during phase 2 and at the start of phase 3, because the domain tables and sync processes still exist there. After cutover, however, writes go to the new domain database, and the monolith's tables fall behind unless those changes also reach them. Rolling back without data loss therefore depends on syncing post-cutover changes back to the monolithic database or reconciling them during the rollback. After the legacy objects are gone, rolling back means restoring them and replaying data changes, which adds a lot of effort and risk. Plan the removal of legacy objects as a deliberate final step for each domain, after the new system has been validated.
Data may also need to flow the other way. Reporting, or components that haven't migrated yet, may still expect the new service's data in the monolithic database. AWS describes a setup where the new service publishes an event to a queue on each update and a synchronizing agent writes those changes into the monolithic database. This can cause data redundancy and eventual consistency between the two stores, and AWS treats it as a tactical step until a long-term solution exists.
Any sync in which application code writes to two targets carries a known trap. If a service writes to its own database and then to a queue as two separate operations (a dual write), a failed second write, or a crash between the two, leaves the data out of step. Our post on data consistency in Event-Driven architectures covers the Transactional Outbox, CDC, and idempotent consumers in detail. The framework in our Single Source of Truth post helps decide which system owns which data during the transition.
The Thoughtworks Event Interception example hands over data ownership in a similar order. The team dark launches the new service and feeds it through CDC. Next, reads go to the new service while writes stay on the legacy system. Then the team intercepts writes and the new service becomes the system of record. Business logic moves last.
Step 5: validate with dark launching, comparison, and canary releases
Before a slice takes real traffic, the team needs evidence that the new implementation matches the legacy behavior wherever that behavior is meant to stay. Three techniques work together here.
Fowler (2020) defines dark launching as calling a new back-end feature in production without showing its results to users, so you can measure the performance impact. Fowler adds that the technique also supports parallel runs of a re-implemented feature: both old and new code get called, their results are compared, and only one answer goes back to the interface. Watch the side effects here. If the slice sends emails, takes payments, or decrements stock, running it on both paths can do the same thing twice, so switch those side effects off on the new path or point them at a test environment.
For comparing results, GitHub's open source Scientist library is a useful reference. It's a Ruby library, and its README lists alternative implementations for PHP, .NET, Python, Java, Node.js, Go, and other languages. The old code runs in a use block (the control) and the new code in a try block (the candidate). Users always get the control's result. Scientist randomizes the order of the two blocks, times both, compares the results, and records any exceptions the candidate raises without passing them to the user. The README states the limit: Scientist can't guarantee the candidate runs every time, so it is only safe for wrapping methods that don't change data. For writes, the README suggests updating the old and new systems together wherever writes happen and comparing results at read time. That double write carries the same dual-write risk described above.
Danilo Sato describes a canary release (2014) as rolling the change out to a small subset of users first, then to the whole infrastructure. Teams can pick the subset at random, start with internal staff, or split by a criterion such as geography. If something breaks, routing users back to the old version is the rollback. One of Sato's caveats applies to strangler migrations as well: if the database schema changes, parallel change can let it support both application versions during the transition.
Step 6: remove the transitional architecture
The facade, the ACLs inside the monolith, mimics, sync agents, and CDC pipelines are mostly there for the migration. Some can stay: Microsoft notes the facade can remain as an adapter for legacy clients, and the Thoughtworks authors keep an ACL toward an external system in the target architecture, calling only the temporary pieces transitional architecture. The term covers software you install to ease the displacement of a legacy system and intend to remove when the work is done. Fowler admits this code can look like waste; in his view, the lower risk and earlier value of the gradual approach outweigh its cost.
The Thoughtworks authors say it may be worth spending a little extra effort while building transitional pieces so they are easy to remove later, because components left in place, even unused ones, can complicate maintenance for future teams. One practical habit: give every transitional component an owner and a written removal condition. For example: "All reads and writes for the user domain run against the new database, and no monolith code touches these tables."
The organizational side of the migration
Fowler and the Thoughtworks authors reach the same point: without changes in organizational culture and leadership, the new system can drift into a similar mess. The fourth activity in Patterns of Legacy Displacement, changing the organization so it can modernize continuously, addresses this risk. AWS also asks teams to weigh compliance requirements, release cadence, team locations, scaling needs, and cognitive load when they draw service boundaries.
The price of the Strangler Fig Pattern is a period where two systems run side by side, plus the transitional architecture that connects them. Answering the Step 1 questions before the first slice, and writing the Step 6 removal conditions with the first transitional component, helps keep that period short.
Sources: Strangler Fig, Martin Fowler (https://martinfowler.com/bliki/StranglerFigApplication.html); Strangler Fig pattern, Microsoft (https://learn.microsoft.com/en-us/azure/architecture/patterns/strangler-fig); Anti-corruption Layer, Microsoft (https://learn.microsoft.com/en-us/azure/architecture/patterns/anti-corruption-layer); Strangler fig pattern, AWS (https://docs.aws.amazon.com/prescriptive-guidance/latest/cloud-design-patterns/strangler-fig.html); Anti-corruption layer, AWS (https://docs.aws.amazon.com/prescriptive-guidance/latest/cloud-design-patterns/acl.html); Patterns of Legacy Displacement (https://martinfowler.com/articles/patterns-legacy-displacement/); Event Interception (https://martinfowler.com/articles/patterns-legacy-displacement/event-interception.html); Legacy Mimic (https://martinfowler.com/articles/patterns-legacy-displacement/legacy-mimic.html); Transitional Architecture (https://martinfowler.com/articles/patterns-legacy-displacement/transitional-architecture.html); Branch by Abstraction (https://martinfowler.com/bliki/BranchByAbstraction.html); Dark Launching (https://martinfowler.com/bliki/DarkLaunching.html); Canary Release (https://martinfowler.com/bliki/CanaryRelease.html); GitHub Scientist (https://github.com/github/scientist); AWS Migration Hub Refactor Spaces (https://docs.aws.amazon.com/migrationhub-refactor-spaces/latest/userguide/what-is-mhub-refactor-spaces.html); Debezium PostgreSQL connector (https://debezium.io/documentation/reference/stable/connectors/postgresql.html); NGINX location (https://nginx.org/en/docs/http/ngx_http_core_module.html#location)