Planning a legacy migration: the rollout is the hard part

MigrationFastAPIGCPSalesforceRollout
Eight columns in which the bright share of traffic on the new system grows from a sliver to the whole column.

The rewrite is not the risky part of a legacy migration. Writing the new system is the part you can estimate. The cutover — moving real traffic and real people onto it without breaking the business — is where migrations actually die, and it is consistently the part that gets planned last.

I have run two migrations that were nothing alike, and the lesson from both was the same one.

Two migrations, two shapes

At Buscaônibus / Travelier, a high-traffic travel search and ticketing platform processing millions of passenger queries, the problem was the runtime. The platform ran on Python 2.7 on App Engine. We moved it to Python 3.12 on FastAPI and CloudRun, containerised. Average API latency came down 25% and GCP infrastructure cost came down 40%.

At Portobello, the problem was not a runtime at all. It was integration: Python and Django services wiring Salesforce to on-prem ERPs and downstream microservices, with Kubernetes CI/CD on Rancher and Jenkins, and an MSSQL Server underneath handling job scheduling and the data pipelines. I led the technical design and stakeholder support for a company-wide Salesforce rollout.

One was a technical migration where the users never knew anything happened. The other was an organisational migration where the software was the easy half. Both hinged on the same thing: how carefully the rollout was planned before anyone wrote code.

Before you touch the code

Find out what actually calls it

Not what the documentation says calls it. What actually does. On a system that has been running for years there is always a consumer nobody remembers: a scheduled job, a partner integration, an internal dashboard, a script on somebody's machine. Instrument the old system and log every caller for a period long enough to catch the monthly ones.

Find the real contract

The real contract is not the API spec. It is the observable behaviour consumers have come to depend on, and it includes the bugs. If an endpoint returns 200 with an empty body where it should return 404, something downstream is almost certainly relying on that. Decide deliberately whether you are preserving each quirk or fixing it, and if you are fixing it, that is a breaking change with its own communication plan.

Establish a baseline you can be held to

Measure the old system before you replace it: latency percentiles, error rate, throughput, infrastructure cost. Percentiles, not averages — an average hides exactly the tail that users complain about.

Why this is not optional

If you have no baseline, you cannot prove the migration worked, and more importantly you cannot tell whether a complaint two months in is a real regression or something that was always broken. "It feels slower" is unfalsifiable without numbers from before.

Migrate in slices, not in a weekend

The big-bang cutover is attractive because it is conceptually simple and because it lets you stop maintaining two systems sooner. It is also the version where you discover every wrong assumption simultaneously, at night, with the business offline.

The alternative is the strangler pattern, which in practice means:

  1. Put a routing layer in front of the old system, so traffic can be directed per endpoint rather than all at once.
  2. Reimplement one endpoint in the new system. Pick the highest-traffic read endpoint with the simplest contract — you want real load early, and you want to be able to reason about correctness.
  3. Shadow it: send production traffic to both, serve the old response, and compare. This is how you find the differences that no test suite predicted, without a user ever seeing them.
  4. Move a small percentage of live traffic over. Watch. Increase.
  5. Repeat, keeping both systems alive until the last consumer has moved.

Running two systems in parallel is genuinely annoying and it is the price of not betting the business on a single evening.

Write the rollback criteria first

This is the part I would defend hardest, because it is the cheapest and the most often skipped.

Before you deploy, write down what would make you roll back. Specific and numeric: error rate above x for y minutes, p95 latency above z, any data-integrity mismatch at all. Write down who is allowed to make that call, and make sure that person does not need to find anyone else at 3am to do it.

Doing this before deployment is not paperwork. It is the only moment you will define those thresholds calmly. Once traffic is on the new system and a graph is moving in the wrong direction, everyone in the room has an incentive to interpret it optimistically — you have all been working on this for months. Pre-committing removes the judgement call from the worst possible moment to make it.

A rollback plan written during an incident is not a plan. It is a negotiation between tired people who want the migration to have worked.

The organisational half

The Portobello rollout is the one that taught me this. Technically it was integration work: Django services, ERP connectors, pipelines, CI/CD. But the thing that determined whether it succeeded was whether people used Salesforce afterwards, and that is not an engineering variable.

The most useful thing I built there was arguably not an integration at all. It was a financial simulator embedded directly in Salesforce, with a Python backend — a tool that gave the commercial team a concrete reason to be in the new system rather than a mandate to be.

That generalises. In any migration where humans change how they work:

  • Find the group that benefits first and migrate them first. You want your earliest reference to be somebody who is happier afterwards.
  • Give people a reason, not an instruction. A feature that only exists in the new system beats a deadline email.
  • Budget for support explicitly. The weeks after cutover need staffed attention, and if you have not planned for it, it comes out of the migration team's evenings.
  • Do not turn the old system off early. The cost of keeping it read-only for another quarter is far smaller than the cost of being wrong about who still needed it.

The checklist

If you are starting one of these, this is what I would want on the wall before any code is written:

QuestionAnswered before you start?
Every real consumer of the old system, from logsYes
Behaviour to preserve vs. bugs to fix, decided per quirkYes
Baseline: p50/p95/p99, error rate, costYes
Rollback thresholds, numericYes
Who can call the rollback, aloneYes
Order of slices, highest traffic firstYes
Date the old system goes read-onlyLater, on purpose

The migration that goes well is boring. Nobody outside the team notices it happened, the graphs move in the right direction, and there is no story to tell afterwards. That is the target.

← All posts Book a 30-min call