Rewriting a legacy application from scratch sounds clean, but it rarely goes that way. The rewrite takes longer than planned, the old system keeps changing in the meantime, and the switch-over becomes one high-stakes night. A safer approach is to replace the system gradually while it keeps running.

Why big-bang rewrites go wrong

  • The old system keeps getting fixes and features, so the new one is always chasing a moving target.
  • Nothing reaches users until everything is done, so you learn about problems very late.
  • The final cutover carries all the risk at once, with no easy way back.

1. Put a proxy in front of the monolith

The strangler pattern, a name Martin Fowler gave to this approach, works by placing a routing layer in front of the old system. Every request goes through it. At first, everything is sent to the legacy app. As you build new pieces, you route just those requests to the new service.

The routing layer can be a reverse proxy such as NGINX or Envoy, or an API gateway. The key benefit is control: you can move a slice of traffic, watch what happens, and move it back in seconds.

2. Migrate in slices

  • Start with a part that is low-risk and fairly self-contained, so you learn the process cheaply.
  • Define a clear boundary around each slice, and put a thin translation layer between old and new so legacy quirks do not leak into the new code.
  • Retire the old code for each slice once the new one has proven itself, so the legacy system shrinks over time.

3. Handle the data carefully

Data is usually the hardest part. Some principles help:

  • One source of truth. At any moment, each piece of data should have one owner. Writing the same data to two systems at once (dual writes) can leave them out of step if one write fails.
  • Prefer safer patterns. Change data capture or the outbox pattern can copy changes reliably from one system to another, and reconciliation jobs can check the two agree.
  • Use expand and contract for schema changes. First add the new structure, then move code to use it, and only then remove the old structure. At every step, both old and new code keep working.

4. Test with real traffic, safely

  • Shadow traffic: send a copy of live requests to the new service and compare its answers to the legacy ones, without affecting users.
  • Canary releases: send a small share of real users to the new version first, then increase the share as confidence grows.
  • Feature flags: turn behaviour on and off without a deployment, so you can react fast.

5. Plan the rollback before the cutover

Decide in advance what "bad" looks like, for example error rate or response time beyond an agreed limit, and automate the switch back when it happens. Keep the old path available until you are confident, and make sure database changes are compatible with going back.

A checklist before each cutover

  1. The new slice has monitoring for errors, latency and business metrics.
  2. Shadow or canary results match the legacy system within agreed limits.
  3. The rollback has been tested, not just written down.
  4. Data in both systems has been reconciled.
  5. The team knows who decides to proceed or roll back, and when.

A note on "zero downtime"

Zero downtime is a goal worth working toward, but it is not something anyone can promise in every case. Some steps, such as certain database changes, may need a short maintenance window. Be honest with your stakeholders about this, plan those moments carefully, and announce them in advance.