Back to writing
Field Notes · Part 2 of 3

The rewrite isn't the hard part. The cutover is.

Change the inside without the outside noticing

AI for Legacy Migration: Field Notes — Part 2 · Safe Cutover

Part 1 was about knowing whether the new code is right. This part is about the question that comes after: how do you put it in front of real traffic when every real thing is still pointed at the old system, and there is no moment when you're allowed to turn it off?

Everything real is pointed at the old system

New code
Written
Tests
Green
Clients
On the legacy system
Payments
On the legacy system
Integrations
On the legacy system
Window
None. It's live.

The rewrite exists. The parity harness says it matches. And none of that has changed anything, because every client, every payment and every third-party integration is still talking to the fifteen-year-old app, and will keep doing so until something moves them.

The greenfield answer is "deploy it". The legacy answer has to account for the fact that the system is in use, right now, by people who did not agree to be part of a migration.

You don't get a maintenance window on a 15-year-old platform. You get a transition.

Mock the upstream you don't control

The new payment portal needed a backend service that didn't exist yet. It was on someone else's roadmap, on someone else's timeline. Waiting for it would have blocked the checkout flow, the card vault and the 3DS2 work, which between them were most of the portal.

So the AI built a mock of the missing service. Not a stub that returns canned data: a working stand-in with the same interface the real service will have, calling the payment gateway directly behind it. Everything above the seam is real. Only the thing below it is temporary.

// appsettings.json
"UpstreamService": "Mock"   // | "Real"
The seam is declared on day one. The swap is designed before the real service exists.
The dependency arrived late. The work never waited for it.

Shadow mode: don't switch. Run both.

A cutover is usually imagined as a switch with two positions. The useful version has three.

Legacy
  • Serves the old path
  • New path runs
  • Differences logged
Shadow
  • Serves the old path
  • New path runs in parallel
  • Every difference logged
New
  • Old path runs
  • Serves the new path
  • Differences logged

Shadow is the position that earns its keep. Users still get the legacy answer, so nothing visible changes. But every request also runs the new path in parallel, and every field that disagrees gets logged: what differed, where, and by how much. You get the full weight of production traffic as a test suite, without a single user being exposed to the result.

Here is what the soak caught on live traffic, mid-migration.

Looked like
6 bugs
  • Six fields diverging between old and new
  • On live traffic, mid-migration
  • Cutover-week nightmare fuel
Traced to
0 bugs
  • Every divergence came from stale test data, not code
  • Investigated field by field
  • Cleared before anyone switched

Six divergences that turned out to be zero bugs is not a wasted week. It is the week you would otherwise have spent after the switch, with customers on the phone, trying to work out which of the six were real.

Production traffic becomes the test suite. Nobody notices.

Shim the old endpoints

External callers were built against the legacy endpoints years ago. You can't make other people redeploy on your schedule, and some of those integrations belong to insurers, who have their own change processes and their own definition of "soon".

External callers
Unchanged. Same code they shipped years ago, same URLs, same request shapes.
→
Legacy surface
Same URLs, same shapes. Now thin shims that translate and forward.
→
New services
The real logic. The only place behaviour actually lives.

The old surface stays up. Thin shims accept the legacy request shape and forward to the new services. The contract is honoured to the byte; the implementation behind it is gone. When an insurer eventually does move to the new API, that's their project, on their timeline, and nothing on our side is waiting for it.

Callers never know the backend changed.

Three patterns, one principle

Each one puts a fixed boundary between something that is changing and something that must not notice. The mock fixes the interface above a service that isn't there yet. Shadow fixes the response users see while the implementation underneath is swapped. The shim fixes the URL and payload shape while everything behind it is replaced.

The boundary holds still while everything behind it moves.

Fast hands, borrowed eyes

The AI writes the mocks, the shadow comparison, the shims. This is faithful-interface work: take an existing contract and produce something that honours it exactly, over and over, for every endpoint and every field. It is tedious for a person and exactly what the model is fast at.

But it is only faithful to interfaces it has read. A mock built from a description of the upstream is a guess with a nice API. A shim built from memory of the legacy endpoint will drift from it in a dozen small ways, and the callers will find every one.

So every one of these patterns starts with the real legacy source in context, never a description of it. That is Part 1's lesson applied: the model doesn't describe the system from memory. It reads it, or it doesn't speak.

Build the seam first. Move the system behind it.

Greenfield ships by deploying. Legacy ships by transitioning, and the transition is a design artefact of its own: the seams, the switch positions, the soak, the rollback path. It deserves the same care as the code it's carrying.

None of it is throwaway, either. The config seam that let the portal ship against a mock is the same seam that will let the next upstream be replaced. The shadow comparison is a parity harness you get to keep. And the seams you cut for the migration become the module boundaries of the system you keep.

Build the seam first. Move the system behind it.

Part 3 coming — the discipline: specs, logs as truth, and a fresh session for every spec.

AI for Legacy Migration: Field Notes
  1. Part 1Most AI coding demos are greenfield
  2. Part 2The rewrite isn't the hard part. The cutover is.
  3. Part 3The discipline: specs, logs as truth, and a fresh session for every spec
Back to writing