The legacy migration playbook: waves, parallel runs, and identical row counts and checksums
Every company has a system nobody wants to touch: an old database, a monolith, two tools that must stay in sync. Big-bang migrations of such systems are where weekends and reputations go to die. The approach that works is boring on purpose: map first, move in waves, run old and new side by side, and switch only when the numbers are identical. This is the playbook I use, with the projects where it held.
11 min read
1. Why big-bang migrations fail
A big-bang migration bets everything on one night: all data, all features, all users switch at once. If something is wrong, you find out in production with no way back that doesn't lose the work done since. The old system also stops getting changes for months while the new one is built, so the business waits.
The projects on this site that went well all avoided that bet. An ERP and a CRM stayed in daily use with the same data while they were connected in both directions. Sensitive health records moved between two public systems with none lost. Tenant databases moved into a Kubernetes cluster with identical row counts and checksums. Different systems, the same pattern.
2. Map the old system first, with AI reading the code
Before any plan, make one map: which components exist, which data they own, which interfaces connect them to the outside, which jobs run at night, and which people and teams depend on what. Old systems hide their real behaviour in places nobody remembers: a stored procedure, a cron job, a CSV export someone opens every Monday.
This is where AI changed the work most. A coding agent can read a large old codebase, list entry points and data flows, and draft the map in days of review instead of weeks of reading. I still verify every important finding against the running system, but the first draft no longer takes the longest. Ask the users too: the export nobody documented is the one that stops payroll.
3. Pick a strategy per component, not one for everything
Not every part deserves a rewrite. AWS's migration guidance lists the common strategies: retire, retain, rehost, relocate, replatform, repurchase and refactor. Use them per component: the reporting module nobody uses can be retired, the stable batch job can be rehosted as it is, the database can be replatformed to a managed service, and only the part that blocks the business gets refactored.
This keeps the expensive work where it pays back. In an automotive project a regulated application moved to Azure Kubernetes Service fully in Terraform, without loosening a single control, and cloud costs came out about 25% lower. The savings came from choosing carefully what to change, not from changing everything.
4. Waves by business capability, behind a façade
Cut the move into waves that make sense to the business: invoicing, then customer accounts, then reporting. Each wave is small enough to test properly and to roll back on its own. Avoid cutting by technology layer ("first the database, then the backend"): a half-moved feature is the hardest state to run.
Put a façade in front, the strangler fig pattern from the Azure Architecture Center: users and other systems talk to the façade, and it routes each feature to the old or the new system. Moving a feature becomes a routing change you can reverse in minutes. Over the waves the old system shrinks until nothing routes to it any more.
5. Moving data without losing a record
Data moves in two phases: a bulk copy of everything, then change data capture that keeps the new side current until cut-over. Managed services such as the Azure Database Migration Service and AWS DMS do both for common databases. Where two systems must stay in step for longer, put a queue between them.
That is how the health-data middleware worked: serverless functions on Azure, with Azure Queue Storage between the two public systems. If one side was unavailable, records waited in the queue instead of getting lost, and were delivered when it came back. Retries, idempotent processing and a dead-letter queue someone actually watches turn "we hope nothing got lost" into "we can show nothing did".
6. Parallel run, row counts and checksums
Before a wave is switched for good, both systems process the same input and you compare the results: totals, invoices, reports, the numbers people actually use. For the data itself, compare per table and per wave: the row count, and a checksum over the content in a fixed order. AWS DMS has built-in validation that compares source and target row by row; for anything else, a query like the one below on both sides does the job.
Agree on the rule beforehand: if counts or checksums differ, the wave does not switch, no matter how close the date is. When tenant databases moved into a Kubernetes cluster with CloudNativePG in the FinOps project, the row counts and checksums were identical before a single tenant was switched.
-- Run on the old and the new database, per table and per wave (PostgreSQL)
SELECT 'invoices' AS table_name,
count(*) AS row_count,
md5(string_agg(md5(t::text), '' ORDER BY t.id)) AS checksum
FROM invoices t;
-- Same result on both sides, or the wave does not switch.7. Cut-over and a rollback you have tested
The cut-over of a wave is a short, rehearsed routine: freeze writes for the wave's data, let the last changes arrive, compare counts and checksums one final time, switch the façade, watch the business metrics. Keep the old side running read-only for a while, so you can look things up and so a rollback is possible.
A rollback plan that was never executed is not a plan. Rehearse it on a copy: switch the façade back, replay the changes made on the new side, check the numbers again. When everyone has seen it work once, cut-over day is calm instead of heroic.
8. Finish the job: decommission and keep it as code
A migration is done when the old system is gone, not when the new one is live. Archive what you must keep for legal reasons, switch the old side off, cancel its licences and contracts, and delete what isn't needed. Otherwise you pay for two systems and the old one keeps a security risk alive.
And build the new side as code from day one, so the next change is a pull request instead of another migration. In the automotive project everything went in as Terraform, with policies that apply to every team: the platform you migrate to should be easier to change than the one you left.
Takeaway
Migrate like an accountant, not a hero: map first, move in waves behind a façade, run both sides in parallel and switch only when row counts and checksums are identical.
Sources and official guides
- Azure Architecture Center: Strangler Fig pattern
- Azure Architecture Center: Anti-corruption Layer pattern
- AWS Prescriptive Guidance: migration strategies
- Azure Database Migration Service overview
- AWS DMS: data validation
- Azure Queue Storage introduction
- Azure Architecture Center: Competing Consumers pattern
- CloudNativePG documentation
More articles
- 3,025 pull requests in ten months: an AI-assisted delivery process, from the inside
- The EU AI Act for mid-sized companies: what applies to you, and when
- FinOps for AI spend: tokens, caching, routing and budgets that hold
- Making a restaurant network visible in AI answers: a GEO project, step by step
- How this site is built: static where it can be, streaming where it must
- Kubernetes cost on EKS and GKE: the same playbook, different levers
- A landing zone for mid-sized companies, in Terraform or Bicep: the parts that matter
- MCP security in depth: how to let AI assistants into your product without opening the doors
- Monitoring on business outcomes: green dashboards, empty checkout
- Phone AI agents: the architecture, and the pitfalls nobody shows in the demo
- Restore tests: a backup you have never restored is only a hope
- Streaming AI answers on Azure Static Web Apps: the one header that buffered everything
- Building an AI assistant for WhatsApp, end to end: what it takes beyond the demo
- Air-gapped systems: running critical software with no internet
- Production AI on AWS: Amazon Bedrock and AgentCore in a private setup
- Production AI on Microsoft Foundry: a private reference setup for agents
- /goal, /loop or routines: stop babysitting your coding agent
- Claude's new plans: Max, Ultra and what they mean for teams
- OpenAI Codex at the end of 2026: goal mode, dots and how to lock it down
- GitHub Copilot, Claude Code or Codex? How I'd choose for a company
- GEO basics: how to show up in AI answers
- Production AI on Google Cloud: from Vertex AI to Gemini Enterprise Agent Platform
- Local LLMs in an air-gapped network: AI for your most sensitive data
- I want to run a local LLM: which server should I buy?
- MiniMax M3: an open frontier model you can run yourself?
- Prompt research at scale with an open model
- SEO and GEO together: a practical audit in 6 steps
- Right-size Kubernetes: the playbook that took a cluster from 7 to 4 nodes
- Private AI for sensitive data: a local LLM setup that passes the security review
- From AI prototype to production: the eight layers I add, in order
