TS-45: Data Migrations
A data migration is the controlled movement of data from one representation, schema, system, or storage location to another. It is one of the higher-risk operations in software engineering. Data is the part of a system that cannot be re-created from source, and a migration touches it directly while the surrounding system continues to operate.
This technical standard covers the principles, strategies, and practices for planning, executing, validating, and rolling back data migrations. It treats two operations as distinct, because conflating them is the most common source of migration failures:
- Data migration. Transforming and copying data into its new representation.
- Switch-over enablement. Pointing the live system at that new representation.
Migrating the data and enabling the switch-over are independent operations. The data can be migrated long before the switch-over, and the switch-over can be rehearsed, deferred, or rolled back without re-migrating anything. Keeping them separate is a core principle of this standard.
For database engines and schema specifics, see TS-43: Relational Databases and SQL and TS-44: Non-Relational (NoSQL) Databases. For release and cutover mechanics, see TS-10: Releasing.
Overview
A data migration is the act of moving data from a source representation to a target representation, where the target is the system’s new intended home for that data. The source and target may differ in schema, storage engine, data model, location, partitioning scheme, or all of these at once.
Data migrations are difficult because data has properties that code does not:
- It cannot be regenerated from source. Lost or corrupted data is lost permanently.
- It accumulates continuously. In a live system, the data set is a moving target.
- It is subject to integrity constraints — internal and external — that must hold before, during, and after the move.
- It is often subject to regulatory and contractual obligations that constrain how, when, and where it may be moved.
Migrating data vs. enabling the switch-over
The single most important distinction in this standard is between the data migration itself and the switch-over that follows it.
- Data migration is the transformation and copying of data from source to target. It is a batch or streaming operation that runs against the data, not against the live system’s request path. It can be paused, resumed, re-run, and validated independently.
- Switch-over enablement is the act of directing the live system — its reads, its writes, or both — to the target representation instead of the source.
These two operations are independent. The data MAY be fully migrated days or weeks before any production traffic touches the target. The switch-over MAY be performed in stages, reversed, or deferred without re-running the migration. Treating "migrate the data" and "cut over to the new system" as a single atomic step is the most common cause of migration failures: it couples two risky operations that have different failure modes, different rollback costs, and different validation criteria.
Note
A migration project SHOULD be planned as two phases with a clear gate between them. The data is migrated and validated first; only when the target is verified as complete and correct does the switch-over proceed, on its own schedule and with its own rollback plan.
When this standard applies
This standard applies to any operation that moves or transforms live data into a new representation: schema refactors, storage-engine changes, system-to-system replacements, data model changes, and platform or cloud migrations. It applies to relational and non-relational stores alike.
It does not cover routine data ingestion or ETL pipelines whose purpose is to feed downstream analytics, nor does it cover backup and restore as a disaster-recovery activity — though the techniques overlap. For release mechanics around the switch-over, see TS-10: Releasing.
Types of data migration
Data migrations fall into a few broad categories. The categories share techniques but differ in scope, risk, and rollback profile, and the strategy SHOULD be chosen accordingly.
Schema migrations
A schema migration changes the structure of data within the same storage system: adding, removing, or renaming columns and tables; splitting or merging records; changing types, constraints, or indexes; repartitioning. Schema migrations are the most common and the most automatable kind, and they are usually handled by a versioned migration tool that applies a sequence of forward and reverse delta scripts.
System migrations
A system migration moves data from one system to another — typically from a legacy system to a replacement, or between two implementations of the same service contract. The source and target often differ in data model, query semantics, and consistency guarantees. System migrations are higher-risk than schema migrations because the two systems can drift while the migration is in flight, and because correctness cannot be reduced to a schema diff.
Storage and platform migrations
A storage or platform migration changes where or how data is physically stored without changing its logical representation — for example, moving from one database engine to another, changing the partitioning scheme, relocating to a different cloud region, or moving from self-managed storage to a managed service. These migrations often preserve the data model but still require full validation, because engine-specific behaviors (collation, ordering, precision, encoding) can produce subtle incompatibilities.
Format migrations
A format migration changes the serialization or encoding of data in place or in transit — for example, converting a column from JSON to a structured type, or re-encoding text. Format migrations are usually a special case of schema migration and SHOULD be handled with the same expand-and-contract discipline.
Categorizing by cutover profile
Independently of the above categories, every migration has a cutover profile that determines its switch-over mechanics:
- Big-bang. The source is quiesced, the migration completes, and the target is enabled in a single coordinated step. Simplest to reason about; highest downtime; highest blast radius on failure.
- Phased. The migration and switch-over proceed in batches (by tenant, by key range, by feature). Lower risk per step; more operational complexity.
- Zero-downtime. The source and target run in parallel through the migration, and the switch-over is performed without interrupting service. Most complex; required when the system cannot be taken offline.
The choice of cutover profile SHOULD be made explicitly, in the planning phase, and justified against the system’s availability requirements.
Principles
The following principles apply to all data migrations, regardless of type or cutover profile.
Reversibility
Every migration step SHOULD be reversible. A reversible step is one that can be undone without data loss and without depending on state that the step itself destroyed.
Reversibility is not the same as rollback of the switch-over. A switch-over that simply repoints traffic from target back to source is cheap; a migration that has destroyed or overwritten source data is not. Migrations SHOULD therefore preserve the source representation, intact and writable, until the target has been validated and the switch-over is stable — typically until a defined soak period has elapsed.
Where a step is genuinely irreversible — for example, a destructive type coercion that loses information — that step MUST be identified explicitly in the plan, gated on validation of the steps that preceded it, and executed only when the cost of a forward-fix is acceptable.
Idempotency
A migration step SHOULD be idempotent: running it twice, or running it against partially-completed state, produces the same result as running it once. Idempotency allows a failed or interrupted migration to be resumed by re-running it, rather than by reconstructing lost state.
In practice this means migration logic SHOULD key off the source data and the target’s existing state, not off a separate record of "what has already been processed." Resume-by-replay is more robust than resume-by-checkpoint.
Testability
A migration MUST be testable before it runs against production data. This requires that the migration logic be separable from the production data path, runnable against a representative copy of the source, and instrumented well enough to verify its result.
Observability
A running migration emits its own observability signals — progress, throughput, error rate, lag — distinct from the production system’s signals. These MUST be visible to the operator on duty, and they MUST be separated from application telemetry so that a migration failure is not lost in the application’s noise.
Minimal blast radius
A migration SHOULD be designed so that a failure affects the smallest possible scope. Batched processing, per-tenant or per-shard isolation, and feature-gated switch-overs all reduce the blast radius of a single failure.
Data integrity first
Preserving data integrity — correctness, completeness, and the validity of every constraint, internal and external — takes precedence over schedule, downtime budget, and implementation cost. A migration that compromises integrity to meet a deadline has failed, even if it appears to have completed.
The migration is not the switch-over
Restated as a principle: the data migration and the switch-over that consumes it are separate concerns, planned, executed, validated, and rolled back on independent schedules. Conflating them is a defect in the plan, not an optimization.
Planning
A data migration SHOULD be planned before any code is written. The plan is the artifact that survives the migration; it is also the document that an operator on duty will reach for at 03:00 when something has gone wrong. It SHOULD be written down, reviewed, and stored alongside the code — see TS-3: Design Docs.
A migration plan SHOULD cover the following.
Discovery and inventory
Before designing the migration, the team MUST establish what data is being moved and what surrounds it:
- The source data set — its volume, growth rate, distribution, and any known anomalies or historical corruption.
- The schema and constraints at the source, including constraints enforced only by application code.
- External dependencies — downstream consumers, upstream producers, foreign keys into and out of the data, and integrations that assume a specific representation.
- Access patterns and load, both steady-state and peak, against the source and the anticipated target.
Mapping and transformation rules
Every field, record, and relationship in the source MUST have a defined target. The mapping SHOULD be explicit and reviewable, including:
- Field-to-field correspondence, including fields that are dropped, split, merged, derived, or synthesized.
- Type coercions and their failure modes — what happens to values that do not fit the target type.
- Encoding, collation, and timezone handling.
- Identity and referential integrity — how primary and foreign keys map across the migration, and how dangling references are handled.
Sizing and timing
The plan MUST estimate the migration’s wall-clock duration and its resource cost, on realistic production-class hardware, using a representative data sample. The estimate SHOULD drive the choice of cutover profile: a migration that cannot complete within an available window MUST be designed as phased or zero-downtime, not as big-bang.
Risk and impact assessment
The plan SHOULD identify, for each step:
- What can go wrong.
- How it will be detected.
- What the rollback is, and what it costs.
- Who is responsible for the decision to roll back vs. forward-fix.
The cutover plan
The switch-over is planned separately from the migration. The cutover plan SHOULD specify, at minimum:
- The exact sequence of enablement steps — reads first, then writes, or both at once; per-tenant or all-at-once.
- The feature flags or configuration that gate each step. See TS-10: Releasing.
- The validation that gates progression between steps.
- The rollback procedure, including the conditions under which it is invoked and who is authorized to invoke it.
- The communications plan — who is notified, when, and through what channel.
Important
The cutover plan MUST be executable independently of the migration. The canonical test of a good cutover plan is that it can be run, fully, against a target populated by a previous migration run, without re-running the migration.
Strategies
This section describes the execution strategies available for a data migration. The strategy determines how the source and target relate while the migration is in flight, and it constrains the available cutover profiles.
Big-bang migration
The source is taken offline or quiesced; the data is copied or transformed into the target in one pass; the switch-over is then enabled against the fully-populated target.
- When it fits. Small data sets, systems with an available maintenance window, and migrations where downtime is acceptable.
- Cost. Simplest to design and validate. Highest downtime. Failure during the migration leaves the system down until rollback completes.
- Rollback. Re-enable the source. Cheap, provided the source was not modified.
Big-bang migrations SHOULD be reserved for cases where the downtime cost is genuinely lower than the engineering cost of a phased or zero-downtime alternative.
Phased migration
The data is partitioned — by tenant, by key range, by geographic region, by feature, or by any other stable sharding key — and each partition is migrated and switched over independently. The partitions move through the migration one at a time, with the rest of the system continuing to operate against the source.
- When it fits. Systems with a natural sharding key, systems serving many independent tenants, and migrations that are too large to complete in a single window but do not require true zero-downtime.
- Cost. Moderate. Requires per-partition routing during the transition, and a clear registry of which partitions have migrated.
- Rollback. Per-partition. A failed partition can be routed back to the source while others continue. This bounded rollback is the principal advantage of the phased approach.
Zero-downtime (dual-write and backfill)
For systems that cannot be taken offline, the migration runs while the source remains live. The canonical pattern, widely known as the "dual-write, backfill, verify, switch" pattern, proceeds in stages:
- Expand. Introduce the target schema or system alongside the source, without directing any traffic to it.
- Dual-write. Begin writing new and updated records to both source and target. The source remains the system of record; writes to the target are additive and must not block or fail the request.
- Backfill. Copy existing source data into the target. The backfill runs in the background, in idempotent batches, and reconciles continuously with the dual-written stream so that the target converges on the source.
- Verify. Compare source and target exhaustively (or by sampling) until they are demonstrably consistent.
- Switch reads. Direct reads to the target, initially for a small fraction of traffic and then ramping. Both source and target continue to receive writes.
- Switch writes. Stop writing to the source. The target becomes the system of record.
- Contract. Remove the source, or keep it as a read-only fallback during a
soak period.
- When it fits. Systems with strict availability requirements, and migrations where the engineering budget supports the added complexity.
- Cost. Highest. The dual-write path must be correct under every failure mode, and the verification step is non-trivial. This is the strategy that most rewards investment in tooling.
- Rollback. Each stage is independently reversible: reads can be repointed back to the source, dual-writing can be resumed, the source can resume its role as system of record. The cost of rollback rises sharply once "switch writes" has executed.
Note
The dual-write path is the failure-prone part of this strategy. Edge cases — out-of-order writes, retries, partial failures, idempotency keys, and the interaction with the backfill — are where correctness is won or lost. The dual-write path SHOULD be exercised against production traffic in a shadow mode before it is relied upon.
Choosing a strategy
The choice of strategy is driven by the system’s availability requirement, the data volume, and the engineering budget:
Strategy | Fits when | Downtime |
|---|---|---|
Big-bang | Small data; downtime acceptable | Full migration duration |
Phased | Large data; natural partition key; partial rollback acceptable | None, per partition |
Zero-downtime | Strict availability requirement | None |
A migration SHOULD default to the simplest strategy that meets the availability requirement. Zero-downtime migrations are not a badge of sophistication; they are a tool for when the simpler strategies cannot meet the constraint.
Schema changes
Schema changes — changes to the structure of data within a single storage system — are the most common form of data migration. They have a well-developed discipline of their own, built around the principle that schema changes MUST be deployable without downtime and reversibly.
Expand-and-contract
The fundamental pattern for zero-downtime schema change is expand-and-contract, sometimes called parallel change. A schema change is decomposed into a sequence of smaller changes, each of which is independently deployable and reversible:
- Expand. Add the new schema elements (columns, tables, indexes) alongside the old. Both old and new representations coexist. The application is deployed in a version that writes to both and reads from the old.
- Migrate. Backfill the new representation from the old, in the background, idempotently.
- Switch. Deploy a version of the application that reads from the new representation. Both representations are still written to.
- Contract. Stop writing to the old representation. After a soak period, remove the old schema elements.
Each step is independently deployable and independently reversible. A failed step is rolled back to the previous step, not to the start of the migration.
Important
Never combine expand and contract into a single deploy. A change that adds the new column, backfills it, switches reads to it, and drops the old column in one release is a big-bang migration in disguise — it has the downtime and rollback profile of a big-bang migration, even if it looks like a schema change.
Compatibility
The application code that runs against the schema MUST be compatible with every schema state it can encounter. In practice this means:
- During the expand phase, the application MUST tolerate the new representation being absent (not yet deployed) or present-but-empty (deployed, not yet backfilled).
- During the switch phase, the application MUST tolerate both representations being present.
- During the contract phase, the application MUST tolerate the old representation being absent.
A common heuristic: any application version SHOULD be compatible with the schema state produced by one schema change forward or backward from the state it was deployed against. This permits independent deployment of schema and application, and it is the precondition for safe phased rollout.
Forward and backward compatibility
A schema change is backward compatible if the new schema can serve old application versions, and forward compatible if the old schema can serve new application versions. Both SHOULD hold during an expand-and-contract migration, so that schema and application can be deployed in either order.
Where forward compatibility is not achievable — for example, when a new column is required by new application logic — the migration MUST sequence the deployments to preserve correctness: expand first, then application, then contract. The plan MUST state this ordering explicitly.
Online schema change tools
For relational databases, prefer online schema change tooling that performs the
change without holding long-running exclusive locks. Examples include native
online DDL where the engine supports it, and tooling that performs
change-by-shadow-copy (gh-ost, pt-online-schema-change, Spirit, and
equivalents). For non-relational stores, the equivalent pattern is a managed
index rebuild or a re-partition that does not block the live workload.
The use of online tooling does not relax the expand-and-contract discipline.
Even an online ALTER SHOULD be deployed in isolation from the application
changes that depend on it, so that each can be rolled back independently.
Versioned migration scripts
Schema changes SHOULD be applied through a versioned migration tool that records each change as an ordered, timestamped delta script with a paired reverse script. The tooling provides:
- A single source of truth for the current schema state of an environment.
- Reproducible application of the schema to new environments.
- A defined reverse path for each step, when one exists.
A reverse script SHOULD accompany every forward script. Where a change is genuinely irreversible (a destructive type coercion, a column drop with no backup), the reverse script MUST be a no-op that emits a clear warning, and the irreversibility MUST be flagged in the plan.
For versioning practices, see TS-11: Versioning.
Execution and cutover
This section addresses the operational execution of a migration and the switch-over that follows it.
Running the migration
A migration run SHOULD be executed as a monitored, controllable process, not as a fire-and-forget job. Specifically:
- Resumable. The migration MUST be resumable after an interruption, without re-processing completed work and without losing in-flight work.
- Throttled. The migration MUST be rate-limited, both to protect the live system from resource contention and to stay within the source and target’s throughput limits. Throttling SHOULD be tunable at runtime, without restarting the migration.
- Paused and resumable. The operator on duty MUST be able to pause and resume the migration without intervention from the migration’s author.
- Logged. Every batch, every error, and every retry MUST be logged with enough context to identify the affected records.
For very large data sets, the migration SHOULD process work in small, idempotent batches keyed off the source data — for example, by primary key range — so that a batch can be retried, skipped, or inspected in isolation. Batches SHOULD be sized so that a single batch’s failure is a recoverable event, not a migration-wide incident.
Idempotency in execution
An idempotent migration step produces the same result whether it runs once or many times. Idempotency is what makes a migration resumable: a failed batch can be re-run safely, and a migration that crashed at 80% can be restarted without reconstructing the lost 20%.
In practice, idempotency requires that the migration’s writes to the target be upserts keyed off the source data, not inserts keyed off a separate processing log. It also requires that the migration tolerate records that already exist in the target — because a previous run created them — and update them to match the source rather than failing.
The switch-over is a separate operation
Once the target is populated and validated, the switch-over is planned and executed as its own operation. The switch-over does not migrate data; it directs the live system to the data that the migration has already placed.
The switch-over SHOULD be staged, with each stage gated on validation:
- Read switch. Direct a fraction of read traffic to the target. Ramp up. Both source and target remain writable.
- Write switch. Stop writing to the source. The target becomes the system of record.
- Soak. Run with the target as system of record, with the source preserved as a fallback, for a defined period — typically days, not hours.
- Decommission. After the soak, retire the source.
Each stage is independently reversible, at declining cost as the migration progresses. Reversing the read switch is cheap. Reversing the write switch is expensive but possible, provided the source remained writable. Decommissioning the source is the point of no return.
Rehearsing the cutover
The cutover plan SHOULD be rehearsed end-to-end against a non-production environment before it is executed against production. The rehearsal exercises the same runbook, the same feature flags, and the same validation gates, against a target populated by a prior migration run. A rehearsal that cannot complete is a strong signal that the production cutover is not ready.
For release mechanics — feature flags, canary rollout, and rollback orchestration — see TS-10: Releasing. For the runbook as a documentation artifact, see TS-25: Technical Documentation.
Communication
A migration and its cutover SHOULD be communicated on a defined schedule to the stakeholders who depend on the affected data — service owners, on-call engineers, and downstream consumers. Communication SHOULD include the planned window, the expected impact, the validation criteria, and the conditions under which the migration will be paused or rolled back.
Validation
Validation is the process of confirming that the target representation matches the source representation, to a defined standard of correctness, before the switch-over is enabled. Validation is the gate between the data migration and the switch-over: the switch-over MUST NOT proceed until validation has passed.
What validation proves
Validation SHOULD establish, at minimum:
- Completeness. Every record that should be in the target is in the target. Every record in the target corresponds to a record in the source (or a defined transformation of one).
- Correctness. Each field in the target holds the value that the mapping and transformation rules dictate for the corresponding source record.
- Integrity. Every constraint — primary keys, foreign keys, uniqueness, application-level invariants — holds in the target.
- Consistency under concurrency. For zero-downtime migrations, the target reflects every source write that has been acknowledged up to a defined point in time, and the lag between source and target is bounded and known.
Validation techniques
Technique | What it establishes |
|---|---|
Row count | Coarse completeness. A necessary but never sufficient check. |
Checksum or hash aggregate | A stronger completeness and correctness signal over a defined subset of fields. Cheap to compute; sensitive to ordering and encoding. |
Record-level diff | Field-level correctness, by comparing source and target records pairwise. Expensive at full scale; typically applied to a sample or to a high-value subset. |
Referential integrity check | That every foreign key in the target resolves. Catches orphaned records and broken mappings. |
Reconciliation against an independent source | That the target agrees with a third system (a billing ledger, an audit log, an analytics warehouse) that does not depend on either the source or the target. |
Replay verification | For zero-downtime migrations, that replaying a captured stream of source writes against the target produces the same result the live dual-write path produced. |
Validation SHOULD combine several of these techniques. A single check — a row count, in particular — is not validation.
Warning
A row count match proves only that the source and target have the same number of records. It says nothing about whether those records are correct. Migrations have shipped with matching row counts and systematically wrong field values. Validation MUST include at least one correctness check, not only a completeness check.
Sampling
Full record-level diffing is often impractical at scale. Where sampling is used:
- The sample SHOULD be stratified — drawn from across the key space, not from the first N records — so that it represents the full distribution.
- The sample SHOULD include known edge cases explicitly: the largest records, the oldest records, records with unusual encodings, records that exercised known bug-fix paths in the source.
- The sampling rate SHOULD be high enough that a systematic error in a large sub-population would be detected with high confidence.
A sample that passes validation does not prove the migration is correct. It raises confidence. The decision to switch over on the basis of a sample SHOULD be recorded, with the sampling design and the residual risk, in the migration plan.
Continuous validation
For zero-downtime migrations, validation is not a one-time gate. The source and target SHOULD be continuously compared — by checksum, by sampling, by reconciliation — through the dual-write and backfill phases, and the divergence between them SHOULD be monitored. A rising divergence is an early signal that the dual-write path is losing writes.
Validation as a gate
Validation is the precondition for switch-over, not a step in the switch-over.
The switch-over plan SHOULD reference validation results by name, citing the
run’s <id> — for example, "switch-over is gated on a passing validation run
identified by that id". The validation run SHOULD be reproducible and
timestamped, so that the gate is auditable after the fact.
Rollback and recovery
A migration that cannot be rolled back is a migration that has decided, in advance, to forward-fix every failure. That decision is sometimes correct, but it MUST be made explicitly in the plan, not arrived at by accident.
Two distinct rollbacks
Keeping the data migration and the switch-over separate yields two distinct rollback operations, with very different costs:
- Switch-over rollback. Repoint the live system from the target back to the source. Cheap and fast, provided the source has remained writable and current. This is the rollback that should be on call during the cutover.
- Migration rollback. Undo the migration itself — restore the source representation that the migration modified or destroyed, or revert the target to its pre-migration state. Expensive, often slow, and sometimes impossible.
A migration plan SHOULD prefer designs in which the cheap rollback (switch-over rollback) is the one that’s needed in the common case. This is the principal reason to keep the source writable and intact through the switch-over and into the soak period.
Backups and point-in-time recovery
A migration that will modify or destroy the source MUST take a verified backup of the source before it begins, and MUST confirm that the backup is restorable — not merely that it was written. A backup that has not been tested for restore is not a rollback plan.
For time-sensitive migrations, point-in-time recovery (PITR) SHOULD be enabled on both source and target, so that either can be restored to a known instant — for example, the moment the migration began, or the moment the write switch was thrown.
Forward-fix vs. rollback
Not every failure should trigger a rollback. A migration that has completed 95% of a large data set, with a correct target and a single broken batch, is often better served by fixing the batch than by reverting the whole migration. The plan SHOULD state, for each step, the conditions under which to roll back and the conditions under which to forward-fix, and SHOULD identify who is authorized to make that call.
As a rule of thumb: roll back when the failure is systematic or unknown; forward-fix when the failure is localized, understood, and bounded.
The irreversible step
Some migration steps are genuinely irreversible — a destructive type coercion that loses information, a column drop without a backup, a source decommission. An irreversible step MUST be:
- Identified explicitly in the plan, in advance.
- Gated on the successful validation of every step that precedes it.
- Executed only when the cost of a forward-fix is acceptable — never as a routine part of the migration.
- Recorded, with the identity of the approver and the reasoning, in the migration’s audit log.
Testing
A data migration MUST be tested before it runs against production. Testing a migration is distinct from testing the application that uses the data.
What to test
A migration test suite SHOULD cover:
- Transformation correctness. Each mapping and transformation rule, exercised against representative source values — including nulls, empty strings, unicode, max-length fields, out-of-range values, and known historical anomalies.
- Edge cases. Boundary values, empty and single-record data sets, records that violate soft constraints, and records that exercise every branch of the transformation logic.
- Idempotency. Running the migration twice produces the same result as running it once, including against partially-populated targets.
- Resumability. Interrupting the migration at representative points and resuming it completes correctly, without re-processing or losing work.
- Failure handling. A failure in a batch, a connectivity loss to source or target, and a partial write to the target are all handled without corrupting the target state.
- Performance and resource cost. The migration’s throughput, memory, and contention profile on production-class hardware, against a realistic data volume.
Where to test
A migration SHOULD be tested against a copy of production data, not against synthetic data alone. Synthetic data rarely reproduces the anomalies, distributions, and scale of real data, and it is precisely those properties that break migrations.
The test environment SHOULD match production in the dimensions that affect the migration: storage engine version, schema, indexes, partitioning, and the volume and distribution of data. Where a full-size copy is impractical, a representative subset — stratified by the same sampling rules used for validation — is preferable to a synthetic data set.
Dry runs and shadow runs
Before a zero-downtime migration runs against production, the dual-write path SHOULD be exercised in a shadow mode: writes are duplicated to the target, but the target is not read by the live system and the duplication cannot affect the request path. Shadow running surfaces dual-write bugs — out-of-order writes, retry hazards, idempotency failures — under real load, before the target is relied upon.
For big-bang and phased migrations, a dry run against a full copy of the source SHOULD be performed end-to-end, including validation, to confirm that the migration completes within its budget and produces a validated target.
Rehearsing the cutover
The switch-over is tested by rehearsing it, end-to-end, against a non-production environment populated by a previous migration run. The rehearsal uses the same runbook, the same feature flags, and the same validation gates as the production cutover.
For testing practices in general, see TS-12: Quality Assurance and TS-13: Functional Testing.
Observability
A running migration is its own workload, with its own failure modes, and it MUST be observable independently of the application it serves. An operator on duty who cannot see the migration’s progress, lag, and error rate cannot safely run the cutover.
For the general principles of observability, see TS-57: Logging, Monitoring, Observability. This section addresses what is specific to migrations.
Migration-specific signals
A migration SHOULD emit, at minimum:
- Progress. Records processed, records remaining, and percentage complete.
- Throughput. Records per unit time, with a baseline so that degradation is detectable.
- Error rate. Errors per batch and per record, categorized so that transient errors (retried) are distinguished from terminal errors (skipped or failed).
- Lag. For zero-downtime migrations, the time between a write being acknowledged at the source and the corresponding write being applied at the target. Lag SHOULD be bounded and the bound SHOULD be alerted on.
- Resource utilization. CPU, memory, I/O, and lock contention on the source and target, attributed to the migration, so that contention with the live workload is detectable.
- Divergence. For zero-downtime migrations, the divergence between source and target as measured by continuous validation. Rising divergence is the leading indicator of a dual-write bug.
Separation from application telemetry
Migration signals SHOULD be presented on their own dashboard, distinct from the application’s dashboards.
Alerting
Alerting on a migration SHOULD be calibrated to its operational profile:
- A stalled migration (no progress for a defined interval) SHOULD alert.
- A rising error rate or rising divergence SHOULD alert, with a threshold below the level at which the migration’s correctness is in doubt.
- A lag breach — lag exceeding the defined bound — SHOULD alert, and SHOULD be treatable as a cutover-blocking condition.
- Resource contention with the live workload SHOULD alert, and SHOULD be treatable as a trigger to throttle or pause the migration.
The audit log
Every migration SHOULD produce an audit log that records, for each batch:
- The batch identifier and the records it covered.
- The result (success, partial, failed, retried).
- The timestamp and the operator action, if any (start, pause, resume, throttle change, manual retry).
The audit log is the record of what the migration did, in what order, and under whose control. It is the artifact consulted after an incident, and it SHOULD be preserved alongside the migration plan.
Risk, security, and privacy
A data migration moves data, often in bulk, often across system or trust boundaries. It therefore concentrates security and privacy risk in a way that routine application operation does not.
Data classification
Before a migration begins, the data being moved MUST be classified according to the organization’s data classification scheme. The classification determines the handling requirements that apply to the migration: encryption, access control, retention, cross-border transfer restrictions, and the regulatory regimes (GDPR, HIPAA, PCI, and others) that govern the data.
A migration plan that does not state the classification of its data is incomplete. See TS-53: Privacy and Data Protection.
Encryption
Data in transit between source and target MUST be encrypted. Data at rest at the target MUST be encrypted to a standard at least equal to the source’s. A migration to a target with weaker encryption than the source is a downgrade and MUST be flagged as such in the plan.
Access control
A migration typically requires elevated access to both source and target — read access to the entire source data set, write access to the target. This access SHOULD be:
- Scoped to the smallest set of permissions the migration actually requires, not a blanket administrative credential.
- Granted for the duration of the migration only, and revoked on completion.
- Audited, with a record of who held it and when.
The migration’s credentials SHOULD be distinct from any long-lived application or operator credential, so that they can be revoked without affecting other work. See TS-52: Security and Secrets Management.
Personal and regulated data
For personal data, the migration plan MUST address:
- Lawful basis and purpose. The migration is a new processing of the data. The organization’s lawful basis for the migration SHOULD be confirmed, not assumed.
- Cross-border transfer. If the migration moves data across jurisdictional boundaries, the transfer mechanism (adequacy decision, standard contractual clauses, binding corporate rules) MUST be in place before the migration begins.
- Minimization. A migration is an opportunity to retire data that is no longer needed. Where retention periods have expired, data SHOULD be deleted rather than migrated.
- Data subject rights. A migration that takes significant time can interfere with data subject access and erasure requests. The plan SHOULD define how such requests are honored during the migration window.
Backups of sensitive data
A backup taken for migration rollback is itself a copy of the data and inherits all of the above handling requirements. Migration backups SHOULD be:
- Encrypted at rest.
- Access-controlled to the migration’s scoped credential.
- Deleted on completion of the soak period, unless they are retained as the production backup.
Insider risk
A migration grants its operators bulk access to data they may not normally see. For sensitive data, the migration SHOULD be designed so that operators do not need to read the data in cleartext to perform the migration — for example, by transferring encrypted exports that the target imports without operator visibility.
Where operator access is unavoidable, it SHOULD be logged and reviewed.
Best practices and anti-patterns
Do
- Separate the migration from the switch-over. Plan them as two operations with a gate between them.
- Preserve the source. Keep the source writable and intact through the switch-over and into the soak period, so that switch-over rollback is cheap.
- Design for idempotency and resumability. A failed or interrupted migration MUST be resumable by re-running it, not by reconstructing lost state.
- Expand and contract. Decompose schema changes into independently deployable, independently reversible steps. Never combine expand and contract in a single deploy.
- Validate before switching over. The switch-over is gated on a passing validation run that includes a correctness check, not only a completeness check.
- Rehearse the cutover. Run the cutover plan end-to-end against a non-production environment before running it against production.
- Test against real data. Synthetic data does not reproduce the anomalies and scale that break migrations. Use a copy of production, or a stratified sample of it.
- Throttle and observe. Run the migration as a monitored, throttleable workload, on its own dashboard, distinct from the application’s telemetry.
- Take and verify backups. A backup that has not been tested for restore is not a rollback plan.
- Classify the data before you move it. The classification determines the handling requirements. Stating it in the plan is non-negotiable for regulated data.
Don’t
- Don’t combine the migration and the switch-over. Coupling them produces a single failure with the worst-case rollback profile of both.
- Don’t trust a row count. A matching row count proves completeness only. Validation MUST include a correctness check.
- Don’t ship a migration you can’t pause. An operator on duty MUST be able to pause, resume, and throttle the migration without the author present.
- Don’t ship an irreversible step unmarked. Irreversible steps MUST be identified, gated on prior validation, and approved explicitly.
- Don’t drop the source on cutover. Decommissioning is a separate step, executed after a soak period, and it is the point of no return.
- Don’t migrate data you don’t need. A migration is an opportunity to retire expired data, not an obligation to carry it forward.
- Don’t treat zero-downtime as a goal. Zero-downtime is a tool for when the simpler strategies cannot meet the availability requirement. Default to the simplest strategy that fits.
- Don’t skip the dual-write shadow run. The dual-write path is where zero-downtime migrations fail. Exercise it under real load before you rely on it.
- Don’t reuse an administrative credential. Scope a dedicated credential for the migration, and revoke it on completion.
References
- Ambler and Sadalage (2006). Refactoring Databases: Evolutionary Database Design. Addison-Wesley. — The source of the expand-and-contract pattern and a foundational reference for evolutionary schema design.
- Stripe (2017). Online migrations at Stripe. — A widely-referenced practitioner account of the dual-write, backfill, and verification pattern for zero-downtime system migrations.