ACE Journal

Zero-Downtime Database Schema Migrations in Continuous Deployment

Abstract

Schema migrations are the most common source of downtime in otherwise zero-downtime deployments. Application code and database schema must stay compatible across the overlap period when both old and new versions of the application are running simultaneously, which is guaranteed to happen during a rolling deployment. The discipline of expand-contract migrations - sometimes called parallel-change - eliminates this compatibility gap by decomposing breaking schema changes into a sequence of backward-compatible steps spread across multiple deployment cycles.

The Expand-Contract Pattern

A breaking migration such as renaming a column cannot be applied atomically in a live system. The expand phase adds the new column alongside the old one. Application code is then updated to write to both columns and read from the new one, with a fallback to the old if the new is null. After all application instances have rolled to the new version, a backfill job populates the new column for all rows written before the dual-write code shipped. The contract phase, deployed in a subsequent cycle, drops the old column once the backfill is confirmed complete and monitoring shows no reads hitting the old column.

This three-phase sequence spans at minimum three deployment cycles. Teams new to the pattern often resist the extra cycles, but the alternative - accepting a migration window and the blast radius of a failed rollback - is worse as deployment frequency increases. At a cadence of ten or more deployments per day, a required maintenance window is simply incompatible with the deployment model.

Tooling and Migration Management

Flyway and Liquibase both support migration scripts that can be structured for expand-contract, though neither enforces the pattern automatically. Bytebase, which reached general availability in 2024, adds a review workflow for schema changes that can enforce custom linting rules, including rules that flag destructive operations (DROP COLUMN, ALTER COLUMN TYPE) in a single migration. Atlas, from ariga.io, takes a declarative approach: the schema is specified as the desired end-state, and Atlas generates the migration sequence, with support for safe migration checks that detect backward-incompatible steps.

For PostgreSQL specifically, operations like adding a nullable column, creating an index CONCURRENTLY, and adding a constraint as NOT VALID followed by VALIDATE CONSTRAINT are all safe to run online. Operations like ALTER TABLE … SET NOT NULL on a large table still require a full table scan and lock in PostgreSQL 14 and earlier; PostgreSQL 15 improved this with NOT NULL constraints that leverage check constraints, which can be added without a full lock.

Coordinating Migrations with Deployments

The migration job must run before the new application version starts serving traffic. In Kubernetes, a Job or init container that runs the migration tool before the Deployment rollout begins is the standard pattern. The migration must succeed before pod readiness gates open. Coupling migration success to deployment readiness prevents the new application code from starting against an incompatible schema.

Rollback planning is as important as forward planning. Migrations must be reversible without data loss through the rollback window - typically one to two deployment cycles. Storing the pre-migration schema snapshot and a tested down-script for each migration in the same PR as the application code change makes rollback an executable procedure rather than an improvised response.