Home / Insights / Architecture and Modernization
Architecture and Modernization
Planning a Zero-Downtime Migration: Compatibility, Cutover and Rollback
“Zero downtime” is a promise you can test only if you define it. This guide turns the principles into a runbook: an SLO-based definition, dependency mapping, expand-and-contract changes, backfill and reconciliation, a staged cutover with go/no-go criteria, and a rehearsed rollback.
TopicsArchitecture and ModernizationQA and Software TestingWeb and SaaS Development
Key takeaways
- Define zero downtime in measurable terms before planning: which user journeys must keep working, the latency and error targets during migration, and the rule that no committed write may be lost.
- Use expand-and-contract (parallel change): add the new structure, migrate clients and data, and remove the old structure only after the new path has run cleanly in production.
- Data is the hard part. Plan backfill, ongoing synchronisation and reconciliation checks — counts, checksums, sampled record comparisons and business invariants — before switching any traffic.
- Cut over reads first, then writes, in small stages controlled by a flag, with written go/no-go criteria for each stage.
- A rollback plan you have not rehearsed is a hope, not a plan. Rehearse it in a production-like environment and time it.
On this page
- The worked example
- Step 1: Define “zero downtime” in measurable terms
- Step 2: Map dependencies and compatibility
- Step 3: Make every change backward compatible
- Step 4: Move the data — backfill, sync and reconcile
- Step 5: Cut over in stages
- Step 6: Write rollback criteria and rehearse them
- Testing strategy before the first stage
- Caches, sessions and background jobs
- Step 7: Communicate and monitor
- Common mistakes
- Runbook template
- Frequently asked questions
- Sources and methodology
Planned downtime used to be normal: announce a maintenance window, stop the system, migrate, start it again. For many products that is no longer acceptable. Customers in different time zones use the system around the clock, integrations expect APIs to respond, and a failed migration discovered at 3 a.m. can mean lost orders and a long recovery.
A zero-downtime migration changes a running system — its infrastructure, database, schema or a whole component — while users keep working. The techniques are well known: blue-green and canary releases, backward-compatible changes, change data capture and feature flags. What teams usually lack is not the vocabulary but a plan that defines success, sequences the work and says exactly when to stop.
This guide is that plan. It uses one worked example throughout and ends with a runbook you can adapt.
The worked example
An order-management service stores orders in a single relational database. The team needs to move the orders data to a new managed PostgreSQL database and, at the same time, change the schema: the free-text delivery_address column becomes structured fields (street, city, postcode, country). The web application, a mobile app, a nightly reporting job and two partner integrations all read or write orders. Peak traffic is around 40 writes per second.
The constraints: checkout must keep working, no order may be lost or duplicated, and partner integrations must not change their requests during the migration.
Step 1: Define “zero downtime” in measurable terms
“Zero downtime” means different things to different people. For one team it means the homepage loads; for another it means no single request fails. Write a definition the business agrees with and the team can test. Google’s SRE guidance on service level objectives offers a useful vocabulary: a service level indicator (SLI) is a measured quantity such as error rate or latency, and a service level objective (SLO) is a target for it.
For the example, the migration definition is:
| Commitment | Measure (SLI) | Target during migration |
|---|---|---|
| Checkout keeps working | Successful order creation requests | At least 99.9% per 5-minute window |
| No noticeable slowdown | p95 latency of order create and read endpoints | Below 600 ms (normal baseline: 350 ms) |
| No lost writes | Orders committed in the old store but missing in the new store after sync | Zero |
| No duplicates | Orders with the same idempotency key in the new store | Zero |
| Partners unaffected | Partner API error rate | No increase above baseline |
| Reporting correct | Nightly report totals compared with the old store | Exact match |
Targets are illustrative. Derive yours from current baselines and what users actually need.
This table becomes the backbone of every go/no-go decision later. If you cannot measure an item today, add the monitoring before the migration starts.
Step 2: Map dependencies and compatibility
Most migration failures come from something nobody listed. Build an inventory of everything that touches the data:
- Readers and writers: services, jobs, scripts, reports, admin tools and partner integrations. Search code, connection strings and database logs; ask operations about manual scripts.
- Implicit contracts: column names used in reports, sort orders, default values, triggers, stored procedures, time zones and character encodings.
- Caches and copies: search indexes, caches, data warehouse extracts and backups that assume the old structure.
- Operational dependencies: monitoring queries, alerts, dashboards and runbooks that reference the old database.
For each dependency, record whether it reads, writes or both, how it will be migrated, and who owns the test. In the example, the inventory found a forgotten finance export that parsed delivery_address with a regular expression. Finding it in week one cost an hour; finding it after cutover would have cost a month-end close.
Step 3: Make every change backward compatible
The core technique is parallel change, also called expand and contract, described by Danilo Sato on Martin Fowler’s site. It has three phases:
- Expand: add the new structure alongside the old one, so both work.
- Migrate: move clients and data to the new structure, gradually.
- Contract: remove the old structure only when nothing uses it.
In the example, that means the new database contains both the structured address fields and, temporarily, the original delivery_address text. The application writes both representations during the transition. The old column is removed only in the contract phase, weeks later.
Changes that remove your rollback path
Some changes are hard or impossible to undo while traffic flows. Schedule them only in the contract phase, after the new path is proven:
- Dropping or renaming columns or tables that any client still uses.
- Changing a column’s type or meaning in place.
- Adding constraints (NOT NULL, unique, foreign keys) before existing data has been cleaned and verified.
- Transforming data destructively, for example splitting the address text and discarding the original.
Schema operations on large tables
Some schema operations lock tables. On PostgreSQL, for example, a standard CREATE INDEX blocks writes until it finishes, while CREATE INDEX CONCURRENTLY avoids blocking writes but takes longer, cannot run inside a transaction block and can leave an invalid index behind if it fails. Plan large operations accordingly:
-- Run outside a transaction block; check for an INVALID index afterwardsCREATE INDEX CONCURRENTLY idx_orders_postcode ON orders (postcode);Check the documentation for your database engine and version: lock behaviour differs between engines and between versions of the same engine.
Step 4: Move the data — backfill, sync and reconcile
Database migration is usually the hardest part because data, unlike code, cannot simply be redeployed. The work has three parts.
Backfill
Copy existing data into the new store in batches. Batch by primary key range, throttle to protect production, record progress so the job can resume, and transform data (here, parsing addresses) in a way that is logged and repeatable. Keep the original text so you can re-run the transformation if the parser improves.
Ongoing synchronisation
While backfill runs and until cutover completes, new writes continue to arrive in the old store. There are two main options:
| Approach | How it works | Strengths | Risks |
|---|---|---|---|
| Dual writes from the application | The application writes to both stores | Simple to understand; no extra infrastructure | Partial failures leave stores inconsistent; ordering problems under concurrency |
| Change data capture (CDC) | A tool reads the old database’s change log and applies changes to the new store | Application unchanged; captures every committed change in order | More infrastructure to run; replication lag must be monitored |
Many teams combine CDC for synchronisation with a later application switch for writes.
The example team chose CDC. Tools such as Debezium read the database’s change log — the binlog for MySQL or a logical replication stream for PostgreSQL — and publish row-level change events, commonly through Kafka, that a consumer applies to the new store. Monitor replication lag as a first-class metric; cutover should not start while lag exceeds an agreed threshold.
Reconciliation
Do not switch traffic until you can show the stores agree. Use several layers of checks, because each catches different problems:
| Check | Example | Catches |
|---|---|---|
| Counts | Orders per day and per status in each store | Missing or duplicated batches |
| Checksums | Hash of key columns per primary-key range | Silent value differences |
| Sampled deep comparison | 1,000 random orders compared field by field, including transformed addresses | Transformation bugs, encoding problems |
| Business invariants | Order totals equal the sum of line items; no orders without customers | Logical corruption the other checks miss |
| Edge cases | Orders updated during backfill; cancelled and refunded orders; very old records | Race conditions and rare paths |
Run reconciliation continuously during the sync phase, not once before cutover.
Shadow reads
Before switching real reads, send a copy of read requests to the new store in the background and compare responses without returning them to users. Shadow reads reveal query differences, missing indexes and latency problems under real traffic, at no risk to users.
Step 5: Cut over in stages
With data in sync and reconciliation clean, switch traffic gradually. Use a feature flag or routing configuration that can be changed in seconds without a deployment.
| Stage | Traffic change | Minimum observation | Go criteria |
|---|---|---|---|
| 0 | Shadow reads only | 2 business days | Response mismatch rate below 0.1%, explained mismatches only; latency within target |
| 1 | 5% of reads from new store | 4 hours including peak | All SLO targets met; no reconciliation failures |
| 2 | 50% of reads | 1 business day | As above |
| 3 | 100% of reads | 1 business day | As above; reporting job verified against old store |
| 4 | Writes switched to new store; reverse sync to old store enabled | Through one full business cycle (for example a week including month-end jobs) | No lost or duplicate writes; partner error rates at baseline |
| 5 | Contract: remove old column, stop reverse sync, retire old store | After sign-off | Dependency inventory shows no remaining users |
Durations are examples. Tie observation windows to your traffic patterns, including peaks and scheduled jobs.
Switching reads before writes means the riskiest change happens last, after the new store has proven itself under real read traffic. Keeping a reverse sync from the new store back to the old one after the write switch preserves a rollback path.
Deployment strategies such as blue-green, canary and rolling releases apply to the application changes along the way. Blue-green switches all traffic between two environments at once; canary releases expose a new version to a small share of traffic first; rolling updates replace instances gradually. None of them, on its own, solves the data problem — which is why data synchronisation and reconciliation come first in this plan.
Step 6: Write rollback criteria and rehearse them
Every stage needs an explicit rollback trigger and an owner who can make the call without a meeting.
| Trigger | Threshold (example) | Action | Decision owner |
|---|---|---|---|
| Error rate | Order create success below 99.9% for two consecutive 5-minute windows | Switch flag back to previous stage | On-call engineering lead |
| Latency | p95 above 600 ms for 15 minutes | Switch back; investigate | On-call engineering lead |
| Data integrity | Any reconciliation failure on committed orders | Stop progression; switch back; freeze contract work | QA lead and data owner |
| Replication lag | Above 30 seconds for 10 minutes before a stage change | Do not proceed | Migration lead |
| Partner impact | Partner error rate above baseline for 15 minutes | Switch back; notify partner managers | Product owner |
Write thresholds before the migration. Decisions made under pressure tend to be optimistic.
Rehearsal
Run the full sequence — backfill, sync, staged cutover and rollback — in a production-like environment with realistic data volumes. Time each step. In the example, the first rehearsal showed that switching writes back required restarting a background worker that cached the connection string; the fix took ten minutes in rehearsal and would have extended a real incident by much longer.
Testing strategy before the first stage
Rehearsal tests the procedure. You also need tests that show the new path behaves correctly. From a QA perspective, the example migration used the following test plan, run in a production-like environment with a copy of anonymised production data.
| Test type | What it covers in the example | Pass condition |
|---|---|---|
| Transformation tests | Address parsing on a large sample of real (anonymised) addresses, including unusual formats | Parsed fields match a reviewed sample; unparseable cases are flagged, never silently guessed |
| Contract tests | Partner API requests and responses before and after the change | Byte-for-byte or schema-equivalent responses for the same requests |
| Regression tests | Order creation, updates, cancellation, refunds and reporting | Existing automated suite passes against the new store |
| Load test | Order writes and reads at twice observed peak (around 80 writes per second) | SLO targets from Step 1 met; replication lag stays below threshold |
| Failure tests | CDC consumer stopped and restarted; new database failover; network delay between stores | No lost or duplicated changes after recovery; lag recovers within the agreed time |
| Rollback test | Writes switched back to the old store after a period on the new store | Reverse sync complete; reconciliation clean; application healthy |
Load targets and pass conditions are examples; base yours on measured peaks and the definition agreed in Step 1.
Pay particular attention to idempotency. During cutover and rollback, the same change may be applied more than once — for example when a CDC consumer restarts from its last committed position. The new store should treat a repeated change as a no-op, typically by keying writes on the order’s primary key or idempotency key and applying updates only when the source version is newer.
Caches, sessions and background jobs
Databases are not the only place where state lives. These components frequently cause problems during cutover:
- Application caches. If the cache holds data in the old format, reads from the new store and cached reads can disagree. Version cache keys during the transition, or flush the relevant keys at each stage change.
- Connection pools and configuration. Long-lived processes may keep connections to the old store after a flag changes. Confirm that every service picks up the switch, including workers that only restart occasionally.
- Background and scheduled jobs. Nightly reports, exports and clean-up tasks may run between stages and read or write the wrong store. List them in the dependency inventory and decide explicitly which store each job uses at each stage.
- Search indexes and downstream copies. If a search index or data warehouse is fed from the old database, plan when its source switches and reconcile it as well.
- DNS and endpoint caching. If the switch involves changing hostnames, reduce DNS time-to-live values well in advance so clients pick up changes quickly in both directions.
Step 7: Communicate and monitor
- Before: tell support, partner managers and stakeholders what will happen, what customers might notice and how to escalate.
- During each stage: a dedicated channel, a named migration lead and a dashboard showing the SLO table from Step 1, replication lag and reconciliation status.
- After cutover: keep heightened monitoring through at least one full business cycle, including month-end or other scheduled jobs that run rarely.
Common mistakes
- Starting without a measurable definition. “No downtime” without SLIs makes every decision a debate.
- Treating deployment as migration. A blue-green switch moves application traffic; it does not move or verify data.
- Reconciling once. A single comparison before cutover misses changes made afterwards. Reconcile continuously.
- Contracting too early. Dropping the old column on cutover day removes the rollback path just when you might need it.
- Ignoring the long tail of readers. Reports, exports and manual scripts break quietly. The dependency inventory is not optional.
- Unrehearsed rollback. Most rollback problems are mundane — cached connections, DNS TTLs, missing permissions — and only rehearsal finds them.
Runbook template
Copy and adapt this outline for your migration:
- Definition: SLI and target table signed off by product, engineering and QA.
- Inventory: every reader, writer, cache, job, report and integration, with owner and test.
- Expand: new schema and infrastructure deployed; application writes both representations or CDC configured.
- Backfill: batch plan, throttling, resume procedure, transformation log.
- Sync: CDC or dual-write design, lag monitoring and alert thresholds.
- Reconcile: counts, checksums, sampled comparisons, invariants and edge cases, running continuously.
- Shadow reads: comparison method and acceptable mismatch rate.
- Cutover stages: traffic percentages, observation windows and go criteria per stage.
- Rollback: triggers, thresholds, decision owners and rehearsed steps with timings.
- Communication: stakeholder list, channel, status updates and escalation path.
- Contract: removal steps, scheduled only after sign-off and a full business cycle.
- Close-out: retrospective, updated documentation and decommissioning of old resources.
Frequently asked questions
Is true zero downtime always worth the effort?
Not always. For internal tools with predictable usage, a short announced window may be cheaper and safer. Use the techniques here when users, revenue or integrations depend on the system being available.
How long should the migration take?
The data move itself may take hours or days; the full expand-migrate-contract cycle often takes weeks, mainly because observation windows and the contract phase should not be rushed.
Should we use dual writes or change data capture?
CDC is usually safer for keeping two stores in sync because it captures every committed change in order without application changes. Dual writes can work for simple cases but need careful handling of partial failures.
Who should own the go/no-go decision?
Name individuals in advance for each type of trigger — engineering for performance, QA and the data owner for integrity, the product owner for customer and partner impact — and give them authority to roll back without a meeting.
Sources and methodology
Facts in this article were checked against the sources below on 6 November 2026. Directory data (ratings, review counts, rate bands and team sizes) changes over time and is shown as published on the date accessed. Illustrative examples are labelled as such and are not quotes.
- Danilo Sato (martinfowler.com): Parallel Change, 13 May 2014 — accessed 6 November 2026. Expand, migrate and contract phases
- Google SRE book: Service Level Objectives — accessed 6 November 2026. Definitions of SLI, SLO and SLA; guidance on choosing targets; error budgets
- Debezium documentation: Architecture — accessed 6 November 2026. CDC from the MySQL binlog and PostgreSQL logical replication; Kafka Connect, Debezium Server and embedded engine
- PostgreSQL documentation: CREATE INDEX — accessed 6 November 2026. Behaviour and caveats of CREATE INDEX CONCURRENTLY
Related reading
Continue your research
Planning a migration you cannot afford to stop?
Tell us about the system, data volumes and constraints. We will help you define success, plan the cutover and rehearse the rollback before anything reaches production.
