Tuba Kaya
3 min read

The Strangler Fig: Modernizing Legacy Without the Big-Bang Rewrite

Why incremental replacement beats the rewrite, and how to route traffic through a facade while you carve a monolith into modular services.

Big-bang rewrites fail for a reason we keep re-learning: they ask the business to stop while engineering rebuilds a system nobody fully understands. The Strangler Fig pattern — named after the vine that grows around a tree until it can stand on its own — offers the opposite bet. You wrap the legacy system, redirect one capability at a time, and let the old system wither only once nothing depends on it.

The shape of the migration

A facade sits in front of the legacy system and decides, per request, whether to serve from the old path or a newly extracted module. Early on almost everything routes to legacy; over time the balance inverts.

legacy routes

extracted routes

extracted routes

Client

Routing

Facade

Legacy Monolith

Orders Service

Billing Service

Orders DB

Legacy DB

The facade is the whole game. It gives you a seam — a place to change behavior without changing either side — and it makes every migration step independently reversible. If the extracted Orders service misbehaves, you flip a flag and route back to the monolith.

Deciding what to strangle first

Pick the capability with the highest change frequency-to-risk ratio: something you touch often but can extract without unwinding the entire data model. A crude scoring pass in Python:

from dataclasses import dataclass

@dataclass
class Capability:
    name: str
    monthly_changes: int      # how often the team edits it
    coupling_score: float     # 0 = isolated, 1 = deeply entangled

    @property
    def priority(self) -> float:
        # Reward frequent change, penalize entanglement.
        return self.monthly_changes / (1 + 5 * self.coupling_score)

candidates = [
    Capability("orders", monthly_changes=18, coupling_score=0.30),
    Capability("billing", monthly_changes=12, coupling_score=0.55),
    Capability("reporting", monthly_changes=3, coupling_score=0.10),
]

for c in sorted(candidates, key=lambda c: c.priority, reverse=True):
    print(f"{c.name:12} priority={c.priority:.1f}")

Implementing the seam

Keep the facade dumb and declarative. Here it is as a small piece of routing middleware in Go, driven by a feature flag so migration is a config change, not a deploy:

// Route sends extracted capabilities to the new service and everything
// else to the legacy upstream. Flipping the flag is instant and reversible.
func (f *Facade) Route(w http.ResponseWriter, r *http.Request) {
    upstream := f.legacy
    if f.flags.Enabled("orders.extracted") && strings.HasPrefix(r.URL.Path, "/orders") {
        upstream = f.ordersService
    }
    upstream.ServeHTTP(w, r)
}

And the flag config that drives it — plain, reviewable, and the actual unit of migration progress:

flags:
  orders.extracted: true      # migrated 2026-07 ✅
  billing.extracted: false    # in progress
  reporting.extracted: false  # not started

The rule that keeps you honest

Every step must leave the system shippable. If a migration step can’t be deployed to production on its own, it’s too big — cut it smaller.

That constraint is what separates the Strangler Fig from a rewrite wearing a disguise. The monolith keeps earning its keep the entire time, and the risk of each step stays bounded to a single capability you can flip back in seconds.