Migrating legacy apps to modern stacks
Strangler-fig pattern: strangle the legacy app route by route or feature by feature, not big-bang. Run old and new side-by-side behind a reverse proxy or feature flag; share auth/session via cookies; migrate the highest-value pages first; back the migration with metrics (perf, bug rate, conversion); kill the legacy code path only when usage is zero.
Big-bang rewrites famously fail (Joel Spolsky's classic "Things You Should Never Do"). The disciplined path is incremental — strangle the legacy app one piece at a time, keep it running, and measure.
The strangler-fig pattern
- Put a reverse proxy (nginx, Cloudflare Workers, a Next.js rewrite, Module Federation, micro-frontend shell) in front of the legacy app.
- New routes / features go to the new app; old ones pass through to legacy.
- Migrate features one at a time. Each migrated route now serves from new.
- When legacy traffic drops to zero, delete legacy.
The user sees one app the whole time.
Shared concerns
| Concern | Strategy |
|---|---|
| Auth / session | Shared cookies on same domain; central auth service; same identity provider. |
| Design parity | Adopt or replicate legacy design tokens in the new DS for the migration window. |
| URLs | Preserve existing URLs so SEO + links don't break. |
| State that's user-facing (cart, drafts) | Persist server-side or in shared storage so users don't lose data crossing routes. |
| Analytics | Same tracking layer or unified events so dashboards stay consistent. |
Which order
Migrate by value × feasibility:
- Highest-traffic pages where perf matters → new stack first.
- Low-risk read-only pages first to build pipeline confidence.
- High-risk transactional flows (checkout) last, with extensive parity testing + feature flag rollout.
Tooling
- Codemods for mechanical migrations (jQuery → React component, class → hooks).
- Visual regression snapshots so users don't notice UI drift across routes.
- Feature flags to flip cohorts (5% → 25% → 100%) on each new page.
- Shadow traffic for high-risk paths — send a copy of prod traffic to the new stack and diff responses.
Metrics
- Web Vitals before/after (LCP, INP, CLS).
- Error rate (Sentry).
- Conversion / engagement (the actual business metric).
- Bundle size.
- Dev velocity (PRs/week, time-to-first-PR for new hires).
If the new stack regresses any of these, fix before migrating more.
Coexistence approaches
- Reverse-proxy routing (simplest; orthogonal apps).
- Module Federation (Webpack 5; share runtime between micro-frontends).
- iframe (last resort; comms via postMessage; bad for SEO + nav).
- Server-rendered shell that mounts legacy + new fragments.
Anti-patterns
- "We'll freeze legacy and rewrite everything for 18 months." Business needs ship in the meantime; legacy diverges; rewrite never lands.
- Rewriting things that work fine just because the stack is old.
- No metrics, so you can't tell if the new stack is actually better.
- Migrating the hardest flow first to "prove" the new stack — proves it works for one flow, blows the timeline.
Interview framing
"Strangler fig — never big-bang. Put a proxy in front and migrate route by route, with shared auth and analytics. Tackle high-traffic read pages first, transactional flows last, behind feature flags + shadow traffic. Measure Web Vitals, error rate, and the business metric on every milestone. Use codemods where possible. Keep legacy alive until usage hits zero, then delete it. The hardest part isn't the new code — it's the contract between the two apps during the migration window."
Follow-up questions
- •How do you share auth between the legacy and new app?
- •What's the trade-off between Module Federation and reverse-proxy routing?
- •When have you killed a migration and rolled back?
Common mistakes
- •Big-bang rewrite that never ships.
- •No coexistence story — users see two apps.
- •Migrating with no metrics.
- •Letting legacy diverge during the freeze.
Performance considerations
- •Each migrated route should improve LCP / INP — track per-route p75. Watch bundle duplication between legacy and new during coexistence.
Edge cases
- •User session crossing between legacy and new mid-flow (cart, multi-step form).
- •SEO redirects when URL shape changes.
- •Acquisition: two whole stacks under one brand.
Real-world examples
- •Etsy's PHP → Node, Slack's jQuery → React, Shopify's monolith → micro-frontends, many fintech migrations.