Skip to content

Web Development · Architecture Patterns

The Strangler Fig Pattern: Migrating a Legacy System Without a Rewrite

A full rewrite of a legacy system is the option that fails most often, not because the new code is bad, but because the business can't stand still for two years. The strangler fig pattern routes traffic to new code piece by piece instead. Here's how it actually works.

Prathviraj Singh

Prathviraj Singh

5 min read

The Strangler Fig Pattern: Migrating a Legacy System Without a Rewrite

Sponsored

Share

Legacy rewrites fail more often from a business problem than a code problem. Freezing feature work for the eighteen months a full rewrite needs isn’t something most companies can actually do, and even when they can, the rewrite’s first real production traffic is also the first moment anyone discovers what it got wrong, after the old system is already gone. The strangler fig pattern exists to avoid both failure modes by never fully committing to the new system until it’s already proven itself, piece by piece, in production.

Where the name comes from

A strangler fig germinates high in the canopy of a host tree, sends roots down to the ground, and slowly envelops the host over years. The host keeps standing and functioning through nearly the entire process, until eventually the fig has replaced enough of its structure that the original tree is no longer needed. Martin Fowler applied the image to software migrations in 2004: build the replacement around the legacy system, let the legacy system keep running everything the replacement hasn’t reached yet, and let the balance shift gradually until the old system does nothing at all.

The mechanism: a routing layer that changes over time

The core piece is a facade or proxy sitting in front of (or inside) the legacy system, deciding per-request which implementation actually handles it.

                    ┌─────────────────┐
  incoming request →│   routing layer  │
                    └────────┬─────────┘

              ┌──────────────┴──────────────┐
              │                              │
     not yet migrated                  migrated piece
              │                              │
      ┌───────▼────────┐            ┌────────▼────────┐
      │  legacy system  │            │  new service /   │
      │                 │            │  new module       │
      └─────────────────┘            └───────────────────┘

For a web application sitting behind a load balancer or API gateway, that routing layer is often literal routing rules:

# API gateway routing config, illustrative
location /api/orders/ {
    proxy_pass http://new-order-service;
}
location /api/ {
    proxy_pass http://legacy-monolith;
}

Everything under /api/orders/ now hits the new service. Everything else still hits the legacy monolith, unchanged. As more functionality gets rebuilt, more routes move from the bottom rule to a specific one above it, and the legacy system’s share of real traffic shrinks with every migrated piece rather than dropping to zero in a single release.

For a monolith you’re decomposing from the inside, before anything is a separate deployable service, the same idea works as a facade module inside the codebase:

def get_order(order_id):
    if order_id in migrated_order_ids():  # feature-flag-driven cutover set
        return new_order_service.get_order(order_id)
    return legacy_order_module.get_order(order_id)

Same pattern, different granularity: a decision point that can route to old or new logic, and a mechanism (a routing table, a feature flag, a percentage rollout) that controls how much traffic goes each way, adjustable without a full deployment of either side.

What this buys you that a rewrite doesn’t

The core advantage is a rollback path at every single step instead of one all-or-nothing cutover at the very end. If the newly migrated order-service has a bug under real production load, you route /api/orders/ back to the legacy monolith while you fix it. Nothing else in the system is affected, because nothing else has moved yet. Compare that to a full rewrite’s cutover day: if the new system has a serious problem, the rollback plan is “revert to the old system entirely,” which is a much bigger and scarier decision to make under pressure, and one many teams end up not making, pushing through known issues instead because reverting feels worse than living with them.

It also means the business never has to freeze feature development on the legacy system for the migration’s duration. New features can still ship to whatever hasn’t been strangled yet, while migrated pieces get their own improvements in the new codebase. A big-bang rewrite typically can’t offer this: feature work either continues on the system being replaced, which means the rewrite target is stale before it ships, or it stops entirely, which the business rarely tolerates for the length of time a real rewrite takes.

What it costs

The honest tradeoff: strangler fig migrations cost more total engineering time than a clean rewrite would in its best-case scenario, because you’re maintaining the routing layer, the legacy system, and the new system all at once for as long as the migration takes. Two systems in production simultaneously means two things to monitor, two things that can have incidents, and real complexity in the routing layer itself, especially anywhere state has to stay consistent across both sides during the transition (a customer record accessible via both old and new code paths needs to actually be the same customer record, not two diverging copies). This is a real coordination cost, not a hypothetical one, and it’s worth designing for from the start rather than discovering it mid-migration. The same discipline applies here as in any system with concurrent access paths that need to agree on the current state, the same class of problem optimistic and pessimistic locking solve at the database layer, just expressed at the level of two whole systems instead of two transactions.

The pattern is worth that cost specifically when the legacy system is large enough, and important enough to the business, that the risk of a single failed cutover outweighs the extra time spent running both systems. For something small enough to safely rewrite in a few weeks, the routing layer’s overhead isn’t worth setting up. For a system where downtime or a broken cutover means real revenue or customer trust lost, incremental and reversible beats fast and risky almost every time.

Frequently asked questions

What is the strangler fig pattern?
An incremental approach to replacing a legacy system where new functionality is built alongside the old system and a routing layer gradually shifts traffic to the new implementation, one piece at a time, instead of rewriting the whole system and cutting over in a single release. The legacy system keeps running and handling everything not yet migrated until the migration is complete.
Why is it called the strangler fig pattern?
It's named after the strangler fig tree, which germinates in the canopy of a host tree, sends roots down to the ground, and gradually envelops and replaces the host over years, while the host continues to function until the very end of that process. Martin Fowler coined the software usage of the term in 2004 to describe exactly this incremental replacement approach.
How is this different from just doing a rewrite?
A rewrite builds the entire replacement system separately and cuts traffic over once it's done, usually months or years later. The strangler fig pattern routes real production traffic to newly migrated pieces as soon as each piece is ready, so you get incremental validation and a working system throughout the migration instead of a single high-stakes cutover at the end.
What does the routing layer actually look like in practice?
It depends on where the legacy system sits. For a web application behind a load balancer, it's often a reverse proxy or API gateway rule set, route /api/orders/* to the new service, everything else to the legacy application. For a monolith you're breaking apart from the inside, it can be a facade module inside the codebase itself, a thin layer that decides per-call whether to invoke old or new logic, before either has moved to a separate deployable service.
When does the strangler fig pattern not make sense?
When the legacy system is small enough that a full rewrite is genuinely faster and lower risk than building and maintaining a routing layer for a matter of weeks, or when the legacy system's architecture is so tightly coupled that no meaningful piece can be extracted and routed independently without touching everything else at once. In the second case, the actual first step usually needs to be decoupling the legacy code enough that pieces become extractable at all, which is real work before any strangling can start.

Sources

Sponsored

Sponsored

Discussion

Join the conversation.

Comments are powered by GitHub Discussions. Sign in with your GitHub account to leave a comment.

Sponsored