ARCHIVED from builddistributedsystem.com on 2026-04-28 — URL: https://builddistributedsystem.com/tracks/migrator/tasks/task-25-1-2-backward-compatible-migrations
TASK

Implementation

A simple "rename column" migration breaks running app instances that still reference the old name. The expand-contract pattern avoids this: first add the new column (both columns exist simultaneously), then backfill the data, then remove the old column once all app instances are updated.

Implement a node that manages backward-compatible schema changes in three phases:

// Phase 1 — EXPAND: add new column (nullable, old column still present)
{ "type": "migrate", "msg_id": 1,
  "phase": "expand", "table": "users", "add_column": "full_name" }
-> { "type": "migration_applied", "in_reply_to": 1,
    "version": 1, "name": "add_full_name_column",
    "schema": "users (name, full_name NULL)",
    "backward_compatible": true }

// Phase 2 — DATA MIGRATION: copy name -> full_name for existing rows
{ "type": "migrate", "msg_id": 2,
  "phase": "migrate_data", "from": "name", "to": "full_name" }
-> { "type": "data_migrated", "in_reply_to": 2,
    "version": 2, "rows_migrated": 1000 }

// Phase 3 — CONTRACT: remove old column (only after all app instances updated)
{ "type": "migrate", "msg_id": 3,
  "phase": "contract", "table": "users", "remove_column": "name" }
-> { "type": "migration_applied", "in_reply_to": 3,
    "version": 3, "name": "remove_name_column",
    "schema": "users (full_name)" }

Sample Test Cases

Expand phase (add new column)Timeout: 5000ms
Input
{
  "src": "admin",
  "dest": "migrations",
  "body": {
    "type": "migrate",
    "msg_id": 1,
    "phase": "expand",
    "table": "users",
    "add_column": "full_name"
  }
}
Expected Output
{"type": "migration_applied", "in_reply_to": 1, "version": 1, "name": "add_full_name_column", "schema": "users (name, full_name NULL)", "backward_compatible": true}
Data migration (backfill)Timeout: 10000ms
Input
{
  "src": "admin",
  "dest": "migrations",
  "body": {
    "type": "migrate",
    "msg_id": 1,
    "phase": "migrate_data",
    "from": "name",
    "to": "full_name"
  }
}
Expected Output
{"type": "data_migrated", "in_reply_to": 1, "version": 2, "rows_migrated": 1000}

Hints

Hint 1
Expand: add the new column as nullable alongside the old one (both exist)
Hint 2
Migrate data: copy/transform values from old column to new column
Hint 3
Contract: remove the old column only after all app instances use the new column
Hint 4
Never rename or drop a column in a single migration with zero downtime
Hint 5
Rolling deployment: deploy new app version to instances one by one, health-checking each
OVERVIEW

Theoretical Hub

Concept overview coming soon

Key Concepts

expand-contract patternbackward compatibilityrolling deploymentcolumn renamezero downtime
main.py
python
Implement Backward-Compatible Schema Migrations - The Migrator | Build Distributed Systems