Back to System Design
System Design
easy
mid

You need to migrate a legacy frontend codebase to a modern framework. What is your plan?

Avoid a big-bang rewrite. Assess and set goals, then migrate incrementally with the strangler-fig pattern — run old and new side by side, migrate route-by-route or feature-by-feature, keep shipping product, add tests as a safety net, and measure progress.

7 min read·~15 min to think through

The headline answer: incremental migration via the strangler-fig pattern — not a big-bang rewrite. Rewrites are high-risk, freeze product delivery, and usually fail or overrun badly.

1. Assess before touching anything

  • Why migrate? Performance, hiring, maintainability, security, ecosystem? The goal shapes the plan and justifies the cost.
  • Map the codebase — routes, features, shared components, dependencies, dead code, the riskiest/most-coupled areas.
  • Pick the target deliberately (React/Next, Vue, Svelte…) — team skills, ecosystem, requirements.
  • Get buy-in — leadership needs to know it's incremental and product keeps shipping.

2. Strangler-fig: new grows around the old

Run the legacy app and the new app side by side, and migrate piece by piece until the legacy is "strangled" out.

  • Routing seam — a proxy/router sends some routes to the legacy app, others to the new one. Migrate route by route (or feature by feature).
  • Or embed — mount new components inside the old app (or vice versa) at boundaries; web components or micro-frontends can bridge frameworks.
  • Shared shell — common header/nav/auth so the seams aren't visible to users.
  • Start with a low-risk, self-contained area to validate the approach and tooling, then move to higher-value areas.

3. Keep shipping product

  • Migration runs alongside feature work, not instead of it — pure-migration freezes don't survive contact with the business.
  • New features get built in the new stack; touched legacy areas get migrated opportunistically.
  • Avoid a long-lived branch — integrate continuously.

4. Safety nets

  • Add tests around legacy behavior before migrating it — characterization/E2E tests on critical flows so you can prove parity.
  • TypeScript, linting, CI gates on the new code.
  • Feature-flag the new implementation; roll out gradually; keep a rollback path.
  • Monitor errors and performance per migrated slice.

5. Watch the tradeoffs

  • Two stacks at once — bigger bundle, duplicated some logic, more complexity during the transition. Accept it as temporary; minimize the window.
  • Shared state/auth across the boundary needs a deliberate bridge.
  • Don't let "incremental" become "forever" — track progress (% migrated) and keep momentum.

6. Finish

  • Migrate the last pieces, delete the legacy code and its dependencies, remove the routing seam and any bridges.

The framing

"I'd resist a rewrite. Assess and set a clear goal, then strangler-fig: run new alongside old, migrate route-by-route behind feature flags with tests proving parity, keep shipping product throughout, and measure progress until the legacy is fully replaced and deletable."

Follow-up questions

  • Why is a big-bang rewrite usually a bad idea?
  • How does the strangler-fig pattern work in practice for a frontend?
  • How do you keep shipping features during a migration?
  • How do you handle shared auth/state across the old/new boundary?

Common mistakes

  • Proposing a big-bang rewrite with a feature freeze.
  • A long-lived migration branch that diverges from main.
  • Migrating without tests, so you can't prove behavior parity.
  • Starting with the hardest, most-coupled area.
  • Letting the migration stall half-done, leaving two stacks forever.

Performance considerations

  • Running two frameworks temporarily inflates bundle size and complexity — keep the transition window short and code-split the seam. Per-slice monitoring catches regressions as each piece moves.

Edge cases

  • Shared global state/auth spanning old and new code.
  • SEO/routing must stay stable through the transition.
  • A legacy area nobody understands anymore.
  • Third-party integrations tightly coupled to the old framework.

Real-world examples

  • Route-by-route migration behind a proxy: legacy serves /old/*, new app serves migrated routes.
  • Embedding new React components into a legacy app via a mount point, expanding outward.

Senior engineer discussion

Seniors reject the rewrite reflexively and lead with strangler-fig: assess and justify, run old/new side by side, migrate route/feature-by-feature behind flags with characterization tests proving parity, and keep product shipping throughout. They're honest about the temporary two-stack cost and stress measuring progress so 'incremental' doesn't become 'permanent.'

Related questions