Skip to content

Data and migrations

Persistent data is not regenerated. It evolves through explicit migrations. Saying “infrastructure handles migrations” is correct only when the responsibility is divided precisely: product intent, artifact generation, execution strategy, and live verification are different jobs.

Each supported revision should produce a normalized physical schema manifest. Comparing it with the parent revision classifies change:

  • no-op: no physical change;
  • safe additive: new table, nullable column, or compatible index;
  • contractive: removal, narrowing, or new non-null requirement;
  • transformational: stored values must change;
  • ambiguous: the physical diff cannot reveal product intent.

For example, removing surname and adding last_name may be a rename, a replacement, or two distinct decisions. Infrastructure must not guess. The model history needs explicit rename intent.

Responsibility Owner
Express rename, split, merge, or transformation Model revision/history
Translate intent into migration artifacts Generator
Choose locks, batches, concurrency, and window Migration planner/orchestrator
Execute with least-privilege credentials Infrastructure
Verify row counts, checksums, and invariants Data verification
Decide product correctness Human approval + model contract

Infrastructure can execute a migration safely; it cannot decide business meaning absent from the model.

An applied migration is durable history. Avoid relying on destructive down migrations as the primary recovery mechanism. Instead:

  • keep application releases temporarily compatible with old and new shapes;
  • switch traffic back only when compatibility is proven;
  • correct bad data or schema with a new forward migration;
  • restore from backup only through an explicit recovery procedure.

Large or high-availability changes use staged evolution:

  1. Expand: add new compatible structures without removing old ones.
  2. Dual compatibility: deploy code that can operate across the transition; use dual write only when required and bounded.
  3. Backfill: copy or transform values in resumable batches.
  4. Verify: compare counts, nullability, checksums, and domain invariants.
  5. Cut over: switch reads/writes to the new shape.
  6. Observe: preserve a compatibility window.
  7. Contract: remove the old shape in a later release.

This is how a “two-hour migration on a live table” is represented. The model captures semantic evolution; the migration plan captures the operational shape.

A production-grade planner should specify:

  • transaction and lock behavior;
  • batch size and checkpoint key;
  • maximum runtime and pause/resume controls;
  • retry and idempotency rules;
  • replica lag or load guardrails;
  • verification queries;
  • compatibility floor for rollback;
  • cleanup in a later release.

Blue-green isolates application instances, not the database by default. Both blue and green may observe the same schema. Therefore the migration must be compatible with the old release until traffic rollback is no longer required.

A traffic switch is atomic; a schema rollback usually is not.

Not every data problem is a schema migration. Corrupt or inconsistent rows may require:

  • idempotent repair jobs;
  • event replay;
  • quarantine and operator review;
  • restore into an isolated environment, verify, then promote data;
  • compensating business actions.

Record evidence and make repair operations observable. Never hide them as edits to generated application files.

See Releases and rollback for compatibility and Infrastructure for the runtime boundary.