Delivery
Zero-downtime schema changes
Use expand and contract to change a database while old and new app versions run together. Add compatible changes first, switch the code, backfill, and remove the old shape last.
Databases are the hard part of a safe deploy. During a rolling release, old and new versions of the app read and write the same tables. The expand and contract pattern keeps both working. You add compatible changes first, move the code over, and remove the old shape last.
Concept
A backfill copies or fixes existing rows in batches while the app keeps running. Dual write means the app writes both the old and the new shape until every reader has moved. Neither one locks the whole table at once.
Walking through the five steps
Here is the safe sequence for adding an email_verified flag to a live users table, while the old code keeps running.
Step 1 Migration: ADD COLUMN email_verified BOOLEAN DEFAULT false
Step 2 Deploy app: dual-write email_verified; read new with fallback
Step 3 Backfill: UPDATE ... WHERE email_verified IS NULL in batches of 1k
Step 4 Deploy app: read and write only email_verified
Step 5 Migration: drop the unused legacy column and stop dual-writeEach step is safe on its own. The old code still works after step 1, because the new column has a default. The new code still works after step 2, because it reads the old data when the new value is missing.
Example
Renaming users.name to users.display_name without downtime. Add display_name, dual-write, backfill, switch the readers, then drop name. A single RENAME COLUMN in the same release as code that only knows the new name breaks the pods that are still running the old code during the rollout.
Large tables and locks
Online schema tools such as pt-online-schema-change, gh-ost and pgroll rewrite large tables without long locks. They build a shadow copy and sync the changes into it. Still rehearse on a production-sized copy, because disk, replication lag and trigger overhead surprise people.
On Postgres, build indexes on large tables with CREATE INDEX CONCURRENTLY. It can fail and leave an invalid index that you must clean up. Do not change a primary key in the same change as an app deploy.
Warning
A 50 ms lock on a 1 GB staging table can be a 20-minute outage on a 2 TB production table. Rehearse with production-sized data whenever a migration locks or rewrites rows.
Migration rules
- Use the same migration tool everywhere. Pick one, such as Flyway, Liquibase, golang-migrate or Prisma Migrate, and run the same ordered files from CI.
- Rehearse on production-sized data when a migration locks or rewrites rows.
- Record versions. Long-lived databases must never skip a version. Ephemeral databases can apply all of them from scratch.
- Pair with flags. Ship the schema change first, then enable the flag that reads the new column. This stops instances on the new code from reading data that is not ready.
Common mistakes
- A single rename in one release. Old pods fail during the rollout.
- Expand and contract in one shot. Old code cannot be used during the gap.
- Building an index without
CONCURRENTLY. The table is locked while it builds.
Check yourself
- Why is a single
RENAME COLUMNrisky during a rolling deploy? Answer: Pods still running the old code expect the old name, so they start failing partway through the rollout. - What is the first step of expand and contract? Answer: Add the new column as a compatible change, nullable or with a default, so the old code keeps working.
Key takeaways
- Expand first, switch the code second, and contract last.
- Backfill in batches, and dual-write until every reader has moved.
- Use online schema tools and
CREATE INDEX CONCURRENTLYon large tables. - Pair schema changes with flags, and rehearse them on production-sized data.
Further learning
The rollback side of the same problem is covered in rollback and roll-forward. For the broader idea of keeping config and state consistent across environments, see configuration and environments.
Related
Deployment strategies and safe releases
Rolling, blue-green and canary deploys, automated rollback, and zero-downtime schema changes - ship often without betting the fleet on every commit.
Configuration, environments and feature flags
Twelve-factor config, environment parity, secrets injection, feature flags and the blast radius of a bad config push.
Rollback and roll-forward
Rollback restores the previous artifact or traffic shape. Roll-forward ships a fix on top of the broken release. Both are valid, and the choice depends on how fast you can restore service and whether the data has changed.