Stackbook Logo
data-stateestablished · medium operational burden

Schema Evolution

Also known as: expand-contract-migration, backward-compatible-schema

Intent

Change data schemas over time without downtime by keeping old and new readers and writers compatible.

Problem

Applications and schemas change continuously, but old and new code versions run side by side during deployments, and persisted or in-flight data lives for years. A breaking change makes rolling deploys, rollbacks or older events fail. Locking migrations on large tables cause outages.

Forces

  • •Multiple code versions run at once during rollout
  • •Rollback must stay possible
  • •Large tables cannot be rewritten under a lock
  • •Stored and streamed data outlives any one release

Solution

Overview

Schema evolution accepts that schema and code never change atomically. Every change must be safe when old and new versions coexist, so changes are staged rather than done in one step.

How it works

Renaming a column safely, in five releases.

1. expand:   ADD COLUMN full_name (nullable)
2. dual-write: app writes name and full_name
3. backfill: UPDATE in batches of 1000
4. read new: app reads full_name only
5. contract: DROP COLUMN name (after old code is gone)

For Protobuf or Avro, add optional fields with defaults, never change a field's type or number, and enforce backward and forward compatibility in a schema registry.

Compatibility has a direction. Backward compatible means new readers can read old data, forward compatible means old readers can read new data, and full compatibility means both. During rolling deploys and with consumers you do not control you need full compatibility, which in practice means only adding optional fields with defaults and only removing fields that were optional. Document the deprecation window, and record the contract date in the ticket that opened the expand step so that it does not get forgotten.

Pitfalls

  • Running a blocking ALTER on a huge table; use online schema change tools or concurrent index builds.
  • Adding a NOT NULL column without a default, which fails or rewrites the table.
  • Never finishing the contract step, leaving permanent clutter.
  • Reusing a deleted field number in Protobuf, which corrupts old data interpretation.

Key takeaways

  • Make each step independently deployable and reversible.
  • Add optional first, remove last.
  • Backfill in small batches with throttling.
  • Automate compatibility checks in CI.

✓ When to Use

  • •Zero-downtime deployments against a live database
  • •Event streams and APIs with long-lived consumers
  • •Teams deploying independently of data consumers

✗ When Not to Use

  • •Offline systems where a maintenance window is acceptable
  • •Throwaway prototypes
  • •One-off data fixes better handled by a single scripted migration

Pros

  • +No downtime and safe rollbacks
  • +Independent deployment of producers and consumers
  • +Compatibility checks catch breaking changes early

Cons

  • −A single change takes several releases
  • −Temporary duplicate fields and dual-write code
  • −Needs discipline and tracking to finish the contract step

Cost Profile

Infrastructure

Low - temporary duplicate columns

Operational

Medium - multi-step rollouts and backfills

Cognitive

Medium - compatibility rules per format

Failure Modes

  • Contract step is dropped too early while an old version still reads the column

  • Backfill locks or overloads the primary database

  • A renamed field is treated as new and old data is silently lost

Real-World Examples

Alternatives

    Related Patterns

    • database-migration
    • api-versioning

    Competency Domains

    economics evolutiondata state