← Blog

August 11, 2026

Running old and new in parallel: coexistence during a partial migration

Why big-bang cutovers fail

Flipping a switch and routing 100% of traffic from the old system to the new one at once assumes the new system is fully correct on day one, for every endpoint, every edge case, every downstream integration. It never is. The alternative — the strangler fig pattern — grows the new system around the old one a piece at a time, with both serving real traffic simultaneously, until the old system has nothing left to do.

Decide the seam: what routes where

The first design decision is where the boundary sits. Pick one, consistently:

  • By endpoint/route. New endpoints go straight to the new service; unmigrated endpoints stay on the old one. Works well when the API surface splits cleanly.
  • By tenant or account. A subset of customers (internal accounts first, then a small external cohort) is served entirely by the new system while everyone else stays on the old one. Good when behavior differs per customer and you want blast-radius control.
  • By traffic percentage. A weighted split (5% → 25% → 100%) sends a slice of all requests to the new system regardless of endpoint or tenant. Good once you trust the new system's correctness and just need confidence at scale.

A reverse proxy, API gateway, or a thin routing layer in front of both services is where this decision lives — not scattered if statements in application code.

The part that actually breaks: shared state

Routing requests is the easy half. The hard half is that both systems usually need to agree on the same data. Three patterns, in order of how much they cost to build:

  1. Single source of truth, one writer. Only one system (old or new) owns writes to the database; the other reads through it or via an API. Simplest, but means the "inactive" system can't be tested end-to-end on writes.
  2. Dual writes. Both systems write to their own store, and a reconciliation job compares them. This surfaces bugs fast but risks silent divergence if reconciliation lags or gets ignored — treat any mismatch as a page-worthy event, not a dashboard nobody looks at.
  3. Shadow traffic. The new system receives a copy of production requests (via the proxy or an async fan-out) and processes them for comparison only — its output is logged and diffed against the old system's real response, but never returned to the user. This is the safest way to validate a new implementation against real traffic with zero user-facing risk, and it's worth doing before any real routing switch, not instead of one.

Idempotency is what makes rollback possible

If a request can be safely retried or replayed against either system without side effects doubling up (double charges, duplicate reconciliation entries, repeated webhook fires), you can move traffic back to the old system the moment something looks wrong, no data cleanup required. Without idempotency, every routing decision is one-way, and one-way decisions are the ones you're afraid to make — which is exactly when migrations stall halfway for months.

Watch the same signal on both sides

Instrument both systems with the same metrics and, ideally, the same dashboard: error rate, latency, and business-level correctness (reconciliation mismatches, failed callbacks). If the new system's dashboard doesn't let you directly compare it to the old one under the same labels, you'll find out about a regression from a support ticket instead of an alert.

Finishing the migration, not just starting it

The coexistence period should have an explicit exit condition decided up front — e.g. "zero reconciliation mismatches for 14 consecutive days at 100% traffic" — not an open-ended "it's mostly working." Once the old system is at 0% traffic, keep it deployed and reachable for a defined rollback window before decommissioning it. Deleting the old system the day traffic hits zero is how a migration that looked done gets undone by the first edge case it didn't handle.