Why Database Migrations Are Risky
Database schema changes are the riskiest part of any deployment. Unlike code changes that can be rolled back instantly by reverting to the previous container image, schema changes alter the persistent state of the system. A failed migration can corrupt data, break running application instances, and require manual intervention to restore service. For a multi-tenant SaaS platform serving thousands of organizations, a migration outage affects every customer simultaneously.
The traditional approach — schedule a maintenance window, take the application offline, run the migration, restart the application — is unacceptable for enterprise SaaS with 99.9%+ uptime SLAs. Zero-downtime migration techniques allow schema changes to be applied while the application continues serving traffic, with no user-visible impact. The trade-off is additional complexity in the migration process, but the alternative — regular downtime windows — is a competitive disadvantage in the enterprise market.
The Expand-Contract Pattern
The expand-contract pattern splits every schema change into three phases. Phase 1 (Expand): add new columns, tables, or indexes alongside the existing schema. The application continues reading from the old schema while writing to both old and new. Phase 2 (Migrate): backfill data from old schema to new schema using a background job. Verify data integrity with checksums. Phase 3 (Contract): once all application instances are using the new schema and backfill is verified, remove the old columns or tables in a subsequent deployment.
This pattern ensures that at no point is the database in a state incompatible with the running application code. Old code reads old columns. New code reads new columns. During the transition, both exist simultaneously. TaptiPM's migration framework generates expand-contract migration pairs automatically: when a developer writes a schema change, the tool produces the expand migration, the data backfill job, and the contract migration as three separate, independently deployable steps.
Backward-Compatible Column Changes
Five common schema changes and their backward-compatible implementations: Adding a column — always use a default value or allow NULL; existing rows get the default, and old application code ignores the new column. Renaming a column — create the new column, add a dual-write trigger, backfill, switch application code, then drop the old column in a later release. Changing a column type — create a new column with the target type, dual-write, backfill with type conversion, switch reads, then drop. Adding a NOT NULL constraint — first add a CHECK constraint that logs violations without blocking, fix violating rows, then add the NOT NULL constraint. Dropping a column — first remove all application code references, deploy, then drop the column in a subsequent migration.
TaptiPM's migration linter validates every migration against these backward-compatibility rules before allowing it to merge. Migrations that rename columns directly, add NOT NULL constraints to populated tables, or alter column types in place are rejected with a recommendation for the safe alternative. This automated guardrail prevents the most common migration accidents.
Data Validation and Rollback Strategies
Every migration should include validation assertions that verify data integrity after the migration runs. Row count comparisons (old table count equals new table count), checksum verification (hashed values match between old and new columns), and referential integrity checks (all foreign keys resolve) catch data corruption before it affects users. TaptiPM runs these validations automatically and halts the migration if any assertion fails.
Rollback for zero-downtime migrations is not a simple "undo" — it is a reverse expand-contract. If the new schema causes issues, the application is reverted to old code (which still works because the old schema remains intact during the expand phase), and the new columns are dropped in a cleanup migration. This is why the contract phase is always a separate deployment: it gives the team a full deployment cycle to verify that the new schema works correctly before removing the safety net of the old schema.
- Zero-downtime migrations use expand-contract pattern: add new, backfill, then remove old in separate deployments
- Never rename columns, change types, or add NOT NULL constraints directly — use safe alternatives
- Migration linters catch backward-incompatible changes before they reach production
- Automated data validation (row counts, checksums, referential integrity) catches corruption during migration
- Contract phase should always be a separate deployment after verifying the expand phase works correctly