The problem
A legacy system is too important to switch off and too costly to keep changing. A big-bang rewrite looks tempting, but it means months or years with no new value, two systems to keep in sync, and one terrifying cut-over day at the end. Many rewrites never reach that day.
How it works
- Intercept. Put a facade in front of the legacy system so that every request passes through something you control.
- Replace one slice. Build the new implementation of a single capability, usually one route or one bounded context.
- Redirect. Change the facade's routing so that slice goes to the new implementation, and redirect calls to it from other legacy modules as well. Legacy code for it then goes dark.
- Repeat, then retire. Keep going until nothing is routed to the old system, then decommission it.
The migration becomes a long series of small, reversible releases instead of one irreversible leap.
When to use it
- Replacing or re-platforming a large system that must stay in service throughout.
- When the system can be intercepted at a boundary: HTTP routes, message topics, a UI shell, file drops.
- When you want to deliver new value during the migration rather than only at the end.
Trade-offs
- Shared data is the hard part. Routes are easy to move, but new services and the monolith often share a database for a while. Plan data ownership, synchronization (for example CDC) and eventual cut-over explicitly.
- The facade is critical infrastructure. It must be highly available and fast, and must not grow business logic of its own.
- Two worlds for a while. Expect duplicated concerns (auth, logging, deployment) until the migration ends. Keep momentum so "temporary" doesn't become permanent.
- Not every system can be sliced. Tightly coupled batch jobs or UIs without seams may need branch by abstraction or a parallel run instead.
Implementation notes
- The facade is often something you already run: an API gateway, an ingress controller, a CDN or load balancer with path-based routing, or a BFF.
- Put an anti-corruption layer between new services and legacy models so the old domain model doesn't leak into the new code.
- Use feature flags or weighted routing to move a route gradually (1% → 10% → 100%), and shadow traffic or a parallel run to compare results before switching.
- Track progress visibly: routes migrated, traffic share on legacy, modules deleted.