Reading time: 7 min Tags: Legacy Systems, Modernization, Architecture, Risk Management, Engineering Strategy

The Strangler Fig Pattern: Modernize Legacy Software One Slice at a Time

Learn how the Strangler Fig pattern helps small teams modernize legacy systems safely by moving functionality incrementally, reducing risk while improving maintainability and delivery speed.

Many teams inherit a legacy application that is both business-critical and fragile. It “works,” but every change feels like surgery: long lead times, risky releases, missing tests, and a growing backlog of small improvements that never happen because the system is too scary to touch.

The usual alternatives—do nothing forever or rewrite everything—both have downsides. Doing nothing slowly taxes every future project. Rewriting everything often fails because the new system takes too long to match the old system’s behavior, edge cases, and data realities.

The Strangler Fig pattern offers a third path: modernize incrementally by building a new system around the old one, then moving functionality over in slices until the legacy core can be retired.

What the Strangler Fig pattern is

The Strangler Fig pattern is named after a plant that grows around an existing tree. In software terms, you keep the legacy system running, but you start redirecting selected requests (or workflows) to new components. Over time, more and more of the system is “handled” by the new architecture, while the legacy portion shrinks.

This is not just “build a new app and copy features.” The pattern is about controlling risk and maintaining continuity: users keep using the product while you swap parts behind the scenes.

At a high level, the structure looks like this:

Client
  -> Routing layer (gateway/proxy/app router)
     -> New services/pages (migrated slices)
     -> Legacy app (everything else, for now)

The routing layer can be a reverse proxy, an API gateway, a BFF (backend-for-frontend), or even application-level routing within a monolith. The exact tool matters less than the discipline: decide explicitly which behavior is “new” versus “legacy,” and make it observable.

Key Takeaways
  • Start with one narrow, valuable “slice” and prove you can route traffic to it safely.
  • Plan for data realities early: shared databases and integration points are where most surprises live.
  • Use feature flags, canary rollouts, and strong observability to reduce migration risk.
  • Make “legacy shrinkage” measurable (which endpoints, pages, or jobs have moved).

Choose the right first slice

The first slice sets the tone. Pick something small enough to ship quickly, but meaningful enough that the team learns real constraints (auth, permissions, data, performance, deployment). If the first slice is too trivial, you’ll delay learning the hard parts. If it’s too big, you’ll recreate the “rewrite” problem.

A concrete example (hypothetical, but realistic)

Imagine a regional logistics company with a legacy web app used by dispatchers. The app includes shipment search, customer management, pricing rules, billing exports, and internal reporting. The reporting page is slow, frequently breaks after schema changes, and is the most-requested improvement.

A good first slice might be “Dispatcher Report: shipments by route for a date range.” It’s read-heavy (lower risk than writes), has clear success criteria (match legacy totals), and has obvious performance goals.

Instead of rewriting the entire app, the team builds a new reporting endpoint and UI, routes only the reporting page to the new component, and leaves everything else on the legacy app.

When selecting your first slice, consider:

  • Frequency and pain: Does it reduce daily frustration or support load?
  • Isolation: Can it be separated at the route/API/job boundary?
  • Data complexity: Can you access the needed data without a giant refactor?
  • Rollbackability: Can you switch back quickly if something goes wrong?

Route traffic safely

Routing is the “control plane” of the Strangler Fig pattern. It decides what runs where, and it’s your primary safety mechanism during migration. Good routing is explicit, testable, and reversible.

Common routing strategies include:

  • Path-based routing: /reports goes to the new service, everything else to legacy.
  • Host-based routing: new.example vs app.example (useful during early adoption).
  • Capability routing: certain API operations (e.g., “quote pricing v2”) go to a new API.
  • Flag-based routing: a feature flag or user cohort determines which backend serves the request.

For small teams, a practical progression is: path-based routing first (simple), then flag-based routing (safer rollouts), then more advanced techniques like canaries and percentage-based traffic splits.

Rollout controls that matter

Regardless of tooling, aim for three controls:

  1. Instant rollback: one switch to route back to legacy.
  2. Limited blast radius: start with internal users, a single customer, or a small percentage.
  3. Comparable outputs: where possible, verify new results against legacy results (even if only in logs).

Data and integration realities

Most modernization pain isn’t UI or APIs—it’s data ownership. Legacy systems often have a single shared database with implicit contracts: stored procedures, ad-hoc queries, and business logic embedded in the schema.

When strangling a system, you typically face one of these data approaches:

  • Shared database (early phase): the new component reads from the legacy database. This is fastest, but you must treat the schema as an unstable dependency and guard against accidental coupling.
  • Replicated read model: the new component builds its own read-optimized store fed by events or change capture. This costs more upfront but reduces load and decouples release cycles.
  • Extracted ownership: the new system becomes the source of truth for a domain (e.g., “customers”), and legacy reads via an API or synced copy. This is the long-term goal, but it’s hard—especially for write-heavy domains.

A common compromise is to start with shared reads for a slice, then introduce a read model once the slice proves valuable and stable.

Also list your integration points early. Legacy apps often connect to payment processors, fulfillment vendors, ERPs, email systems, and scheduled jobs. Even if your first slice doesn’t touch them, you need a map of what will eventually be impacted so you don’t “discover” a critical nightly export after a migration breaks it.

Make progress visible: tests and observability

The Strangler Fig pattern works because it reduces uncertainty. That only happens if you can observe behavior and compare old vs new. Treat observability as a feature of the migration, not an afterthought.

At minimum, for each migrated slice, define:

  • Service-level indicators: latency, error rate, and throughput for the new endpoints/pages.
  • Business checks: counts, totals, or invariants that must match expected outcomes (for example, “report totals match legacy within tolerance”).
  • Operational runbook: where to look when it fails, and how to route back to legacy.

Testing should focus on preserving external behavior. You don’t need to copy the internal design of the legacy system; you need to match what users rely on. A good tactic is to document “behavioral contracts” for the slice: inputs, outputs, permissions, and edge cases.

Common mistakes and how to avoid them

  • Moving the mess: teams copy legacy logic verbatim into the new system, including odd edge-case hacks. Instead, capture behavior with tests, then simplify deliberately—one rule at a time, with stakeholders aligned on changes.
  • Ignoring authorization differences: old apps often have inconsistent permission checks. Ensure the new slice enforces permissions at the same boundary (and add audits for sensitive operations).
  • Creating a “routing mystery”: if nobody knows which requests go where, debugging becomes painful. Maintain a clear routing map in documentation and dashboards.
  • Letting the legacy system keep growing: if new features continue to land in legacy, your target keeps moving. Adopt a rule: new work goes to the new platform whenever feasible, even if it’s a thin wrapper at first.
  • Skipping decommission steps: once a slice is stable, teams forget to delete the legacy code path. Schedule explicit “retire the old path” work, including permissions, cron jobs, and documentation.

When NOT to use this approach

The Strangler Fig pattern is powerful, but it’s not always the right tool. Consider avoiding it when:

  • The legacy system is small and well-tested: a focused refactor may be cheaper than building parallel infrastructure.
  • You can’t route cleanly: if everything is tightly coupled with no stable boundaries (no separable routes, no module seams), you may need an internal modularization step first.
  • Regulatory or validation constraints demand a big-bang cutover: some environments require full-system certification as a unit, making incremental replacement difficult.
  • Your team can’t support two systems: for a time, you will run legacy and new in parallel. If operational capacity is already maxed out, stabilize operations first.

A useful gut check: if “keeping two systems running” sounds impossible, start by reducing operational load (simplify deployments, add monitoring, reduce on-call pain) before starting a strangler migration.

A copyable checklist for your next migration slice

Use this checklist to plan and ship one slice without turning the effort into a hidden rewrite.

  • Define the slice boundary: route, page, endpoint, or job that can be switched independently.
  • Write a “behavior contract”: inputs/outputs, permissions, edge cases, and error behavior.
  • Choose data strategy: shared read, replicated read model, or extracted ownership (explicitly decide).
  • Add routing control: a switch for rollback plus a way to limit rollout (cohort or percentage).
  • Instrument before rollout: logs/metrics for latency and error rate; add at least one business correctness check.
  • Run side-by-side validation: compare old vs new results for a sample set (automated if possible).
  • Roll out gradually: internal users → small cohort → wider rollout.
  • Retire the legacy path: remove old routes, dead code, and any related cron jobs once stable.
  • Update the system map: keep a single source of truth for what’s migrated and what remains.

Conclusion

The Strangler Fig pattern is a practical way to modernize legacy software without betting the company on a rewrite. By routing a narrow slice to new components, validating behavior, and iterating, you can steadily reduce risk while improving maintainability and delivery speed.

The key is to treat each slice as a complete product change: clear boundary, safe rollout, observable behavior, and an explicit retirement of the legacy path. Do that repeatedly, and modernization becomes a habit instead of a heroic event.

FAQ

How long does a Strangler Fig migration take?

It depends on slice size and team capacity, but the pattern works best when slices ship regularly. Aim for a first slice that can be delivered in weeks, not quarters, so you can validate your routing, deployment, and data assumptions early.

Do I need microservices to do this?

No. The “new system” can be a modular monolith or a single new web app. The core requirement is independent deployment (or at least independent routing) for the migrated slice.

Should the new system share the legacy database?

Sometimes, especially at the beginning. Shared reads can accelerate delivery, but be deliberate: document which tables you depend on, and plan a path toward decoupling if the slice becomes strategic or write-heavy.

How do we handle inconsistent legacy behavior?

Capture what users rely on as a behavioral contract, then decide what to preserve versus what to fix. Where you intentionally change behavior, treat it as a product decision with explicit acceptance criteria rather than an accidental “bug fix during migration.”

This post was generated by software for the Artificially Intelligent Blog. It follows a standardized template for consistency.