diff.expert
TechniquesExercisesReference
© 2026 diff.expertAn Arbiter course
Home/Techniques/Strangler Fig
Technique 7

Strangler Fig

Grow the new system around the old one, one vine at a time.

←PreviousParallel ImplementationNextData Migration Decoupling→

When to Use

You're migrating a large system or subsystem and want to do it piece by piece, routing between old and new at the boundary. Unlike Branch by Abstraction (which swaps implementations behind a single interface), Strangler Fig works at the system boundary level — you're replacing entire vertical slices of functionality.

You'll recognize the need when you have a legacy module or application that needs to be rewritten, but a big-bang rewrite would take months and carry enormous risk. The pattern names itself after the strangler fig tree, which grows around its host tree, eventually replacing it entirely while the host continues to support it during the transition.

Use this technique when:

  • A legacy system needs incremental replacement — it's too large to rewrite at once, and too risky to run two systems in parallel without a routing layer
  • Vertical slices can be migrated independently — each page, route, or user-facing feature can be replaced one at a time without blocking the others
  • You need to deliver value during the migration — each migrated slice ships to production immediately, letting you validate the new system in the real world before the full cutover

The Pattern

  • PR 1 — Routing layer: Introduce a proxy or router at the boundary between the system and its consumers. All traffic flows through the router to the legacy system. This is a no-op in terms of behavior — a pure structural addition that gives you the switching point you need.
  • PR 2–N — Migrate one slice at a time: Migrate one capability (a route, a page, a user-facing feature) to the new system. Each PR updates the routing layer to direct that capability's traffic to the new implementation. The legacy system still handles everything else.
  • PR N+1 — Remove the proxy: Once every route points to a new implementation, the routing layer is redundant. Remove it and wire consumers directly to the new system. Delete the legacy code.

The discipline is in migrating vertically — one complete user-facing slice at a time — rather than horizontally (all the database layer first, then all the business logic). Vertical slices ship value immediately.

Commit Sequence

How this looks in your git history:

Worked Example

The team has a legacy AdminPanel module built with class components and direct DOM manipulation. It handles four admin pages: user management (/admin/users), role assignment (/admin/roles), audit logs (/admin/audit), and settings (/admin/settings). The team is migrating to modern React (hooks, TypeScript, server components) one page at a time.

The migration plan: first introduce an AdminRouter that delegates all traffic to the legacy panel (the strangler fig root), then migrate each admin page to a new implementation one by one, updating the router each time. After all four pages are migrated, remove the router entirely and render the new pages directly.

1

PR 1: Add AdminRouter to delegate between legacy and new admin pages

Create AdminRouter — a thin routing layer that sits between the app and the legacy AdminPanel. The router inspects the current path and delegates to the matching handler. Right now, all four routes map to 'legacy', which renders the original AdminPanel.

This PR is a pure no-op: all behavior is identical. The AdminPanel renders exactly as before. What we've gained is the switching point — a single place where we can redirect each route to a new implementation, one at a time, without touching any consuming code.

src/components/admin/AdminRouter.tsx
line numberline content
2

PR 2: Migrate user list page to new React implementation

Create UserListPage — a modern functional component with hooks and TypeScript — and update the routing table to send /admin/users to it. The diff here shows the new component; the router change is a one-line swap of 'legacy' for the import.

The legacy AdminPanel still handles /admin/roles, /admin/audit, and /admin/settings unchanged. Users on those pages see nothing different. Users on the /admin/users page now see the new implementation. One vertical slice shipped. Three to go.

src/components/admin/UserListPage.tsx
line numberline content
3

PR 3: Migrate role assignment page to new React implementation

Create RoleAssignmentPage and update the routing table to send /admin/roles to it. The pattern is identical to PR 2: a new functional component, a one-line router update.

The legacy AdminPanel now handles only two routes: /admin/audit and /admin/settings. Two slices migrated, two remaining. The routing table clearly shows the migration status at a glance — comments mark the remaining legacy routes.

src/components/admin/RoleAssignmentPage.tsx
line numberline content
4

PR 4: Migrate audit log and settings pages — legacy is now unreachable

Create AuditLogPage and SettingsPage, and update the routing table to remove the last two 'legacy' entries. The diff here shows the AdminRouter change — the routing table now points every route to a new implementation.

The legacy AdminPanel is now unreachable. No path through AdminRouter leads to it anymore. The code still compiles; the import is still there. But no user will ever render it again. This "unreachable but present" state is intentional — it lets you keep the legacy code available as a fallback for one more PR cycle before the final cleanup.

src/components/admin/AdminRouter.tsx
line numberline content
5

PR 5: Remove legacy AdminPanel and routing layer — clean final state

Delete AdminPanel entirely and remove AdminRouter. The consuming page now renders each admin component directly based on the route — no indirection layer.

The diff shows src/app/admin/page.tsx before and after: the AdminRouter wrapper is replaced with a direct route-to-component map, and the legacy import is gone. The AdminPanel directory is deleted from the repo.

Clean final state: no legacy code, no routing indirection, no TODO comments. The strangler fig has fully replaced its host. Every new component is battle-tested because it has been running in production since its migration PR.

src/app/admin/page.tsx
line numberline content

Common Mistakes

Migrating horizontally instead of vertically. It's tempting to migrate "all the database access first, then all the business logic, then all the UI" — because that seems organized. But this approach gives you nothing to ship until the very end. Horizontal migration means users get no benefit until the full rewrite is complete; any slip in one layer blocks the entire sequence. Vertical slices — one complete user-facing capability at a time — let you ship value and validate the new system in production after each PR. Migrate the /admin/users page end-to-end: new API, new logic, new UI. Then /admin/roles. Each one is done.

Underinvesting in the routing layer. A well-designed routing layer (like the ROUTES table in this example) makes each migration PR trivially small — update one entry, add one import. A poorly designed routing layer — scattered if/else chains, duplicated route strings, or a layer that leaks implementation details — creates friction that makes engineers reluctant to migrate the next slice. Spend the time in PR 1 to make the switching mechanism clean and obvious. That investment pays off across every subsequent migration PR.

Leaving the routing layer in place after migration is complete. Once all routes point to new implementations, the proxy is pure overhead: an extra layer of indirection, an extra place for bugs to hide, and dead code that future engineers will waste time understanding. Remove it in the cleanup PR (the final step). Don't let the router become a permanent fixture — the whole point was to use it as a bridge and then burn the bridge once you've crossed.

←PreviousParallel ImplementationNextData Migration Decoupling→
← All techniques