Ink&Horizon
HomeBlogTutorialsLanguages
Ink&Horizon— where knowledge meets the horizon —Learn to build exceptional software. Tutorials, guides, and references for developers — from first brushstroke to masterwork.

Learn

  • Blog
  • Tutorials
  • Languages

Company

  • About Us
  • Contact Us
  • Privacy Policy

Account

  • Sign In
  • Register
  • Profile
Ink & Horizon

© 2026 InkAndHorizon. All rights reserved.

Privacy PolicyTerms of Service
Back to Blog
Database

Zero-Downtime Database Migrations: The Complete 2026 Guide

Expand-contract pattern, Flyway, Prisma Migrate, blue-green schemas, and safe migration strategies

2026-02-10 20 min read
ContentsWhy Migrations Are DangerousThe Expand-Contract PatternSafe Migration OperationsMigration Tools: Flyway vs Prisma MigrateRollback StrategiesKey Takeaways

Why Migrations Are Dangerous

Database migrations are the only deployment step that cannot be instantly rolled back. You can roll back application code in seconds with a container swap, but a migration that drops a column, truncates data, or acquires an exclusive lock can cause irreversible damage or hours of downtime.

The core problem: during deployment, the OLD version of your application and the NEW version must both work with the database schema simultaneously. If a migration adds a NOT NULL column, the old code crashes because it does not include that column in INSERT statements.

Zero-downtime migrations solve this with the Expand-Contract pattern: first, expand the schema to support both old and new code. Deploy the new code. Then, contract (clean up the old columns/tables). Never do both in a single deployment.

Key Takeaways

Migrations cannot be instantly rolled back like application code.
During deployment, OLD and NEW code must both work with the schema.
Expand-Contract: expand schema → deploy code → contract schema.
Never combine schema changes and code changes in one deployment.
One bad migration can cause hours of downtime — test in staging first.

The Expand-Contract Pattern

The Expand-Contract (also called Parallel Change) pattern splits dangerous migrations into safe, incremental steps. Instead of renaming a column in one migration (which breaks old code instantly), you add a new column, sync data between both, deploy code that uses the new column, then drop the old column.

This pattern applies to: column renames, column type changes, table renames, NOT NULL additions, and any change that would break existing code.

Snippet
-- EXAMPLE: Rename column "name" → "full_name" safely
-- (Direct rename would break all existing code instantly!)

-- Step 1: EXPAND — Add new column alongside old one
ALTER TABLE users ADD COLUMN full_name TEXT;
-- Backfill existing data
UPDATE users SET full_name = name WHERE full_name IS NULL;
-- Add trigger to keep both columns in sync during transition
CREATE OR REPLACE FUNCTION sync_user_name() RETURNS TRIGGER AS $$
BEGIN
  IF NEW.name IS DISTINCT FROM OLD.name THEN
    NEW.full_name = NEW.name;
  END IF;
  IF NEW.full_name IS DISTINCT FROM OLD.full_name THEN
    NEW.name = NEW.full_name;
  END IF;
  RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER trg_sync_name BEFORE UPDATE ON users
FOR EACH ROW EXECUTE FUNCTION sync_user_name();

-- Step 2: Deploy new application code that reads/writes "full_name"
-- Old code still works because "name" column exists and syncs

-- Step 3: CONTRACT — After confirming new code works, clean up
DROP TRIGGER trg_sync_name ON users;
DROP FUNCTION sync_user_name();
ALTER TABLE users DROP COLUMN name;

Key Takeaways

Never rename/drop a column in one step — expand first, contract later.
Use a sync trigger to keep old and new columns consistent during transition.
Backfill existing data before deploying new code.
Wait at least one full deployment cycle before contracting (cleaning up).
Each step is a separate migration that can be independently verified.

Safe Migration Operations

Some operations are always safe (additive changes), some are always dangerous (destructive changes), and some depend on the execution strategy. Knowing which is which prevents outages.

Always safe: ADD COLUMN without NOT NULL default, CREATE TABLE, CREATE INDEX CONCURRENTLY, ADD CONSTRAINT with NOT VALID.

Always dangerous: DROP COLUMN, DROP TABLE, RENAME COLUMN, ALTER TYPE on populated columns, ADD COLUMN with NOT NULL without default.

Conditionally safe: ADD INDEX (use CONCURRENTLY), ADD NOT NULL (add with default first, then apply constraint), ALTER TYPE (expand-contract).

Snippet
-- ✅ SAFE: Add nullable column (no lock, instant)
ALTER TABLE posts ADD COLUMN subtitle TEXT;

-- ✅ SAFE: Add column with default (PG 11+ — instant, no rewrite)
ALTER TABLE posts ADD COLUMN views INTEGER DEFAULT 0;

-- ❌ DANGEROUS: Add NOT NULL without default (fails for existing rows)
ALTER TABLE posts ADD COLUMN category TEXT NOT NULL;

-- ✅ SAFE: Add NOT NULL in two steps
ALTER TABLE posts ADD COLUMN category TEXT DEFAULT 'general';
-- After backfilling existing rows:
ALTER TABLE posts ALTER COLUMN category SET NOT NULL;

-- ✅ SAFE: Create index without blocking writes
CREATE INDEX CONCURRENTLY idx_posts_category ON posts(category);

-- ❌ DANGEROUS: Create index without CONCURRENTLY (locks table!)
CREATE INDEX idx_posts_category ON posts(category);

-- ✅ SAFE: Add foreign key without blocking writes
ALTER TABLE posts 
  ADD CONSTRAINT fk_author FOREIGN KEY (author_id) REFERENCES users(id)
  NOT VALID;  -- Skip checking existing rows
-- Validate in separate step (doesn't lock table)
ALTER TABLE posts VALIDATE CONSTRAINT fk_author;

Migration Tools: Flyway vs Prisma Migrate

Flyway is the industry standard for SQL-based migrations. It uses versioned SQL files (V1__create_users.sql, V2__add_posts.sql) and tracks applied migrations in a metadata table. It is database-agnostic and works with any SQL language.

Prisma Migrate is the modern TypeScript-first alternative. You define your schema in Prisma's DSL, and it generates SQL migrations automatically. It provides type-safe database access through the Prisma Client. Better DX but less control over the SQL.

For Spring Boot applications (like InkAndHorizon), Flyway is the standard choice — it integrates directly with Spring Boot's startup lifecycle.

Snippet
-- Flyway: V1__create_users_table.sql
CREATE TABLE users (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  email VARCHAR(255) NOT NULL UNIQUE,
  name VARCHAR(100) NOT NULL,
  role VARCHAR(20) DEFAULT 'user',
  created_at TIMESTAMPTZ DEFAULT NOW(),
  updated_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_users_role ON users(role);

-- V2__add_posts_table.sql
CREATE TABLE posts (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  title VARCHAR(200) NOT NULL,
  content TEXT NOT NULL,
  author_id UUID NOT NULL REFERENCES users(id),
  published BOOLEAN DEFAULT false,
  created_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX CONCURRENTLY idx_posts_author ON posts(author_id);

-- Flyway configuration (application.yml for Spring Boot)
-- spring:
--   flyway:
--     enabled: true
--     baseline-on-migrate: true
--     locations: classpath:db/migration
--     validate-on-migrate: true

Key Takeaways

Flyway: versioned SQL files (V1__, V2__). Industry standard. SQL control.
Prisma Migrate: schema-first DSL, auto-generated SQL. Better DX.
Always use versioned migrations — never use ddl-auto: update in production.
Test every migration in staging before production.
Keep migrations forward-only — never edit an already-applied migration.

Rollback Strategies

The best rollback strategy is: do not need one. If you follow expand-contract, every migration is additive and safe. Old code and new code both work with the expanded schema, so rolling back the application code requires no schema changes.

For emergencies, maintain a "down" migration for every "up" migration. The down migration should reverse the schema change. But in practice, down migrations are dangerous because they run in reverse order and may fail on real data.

The nuclear option: restore from a point-in-time backup. PostgreSQL's WAL-based PITR (Point-in-Time Recovery) can restore to any moment within your retention window. This is your last resort — test it regularly.

Key Takeaways

Best rollback: expand-contract makes rollbacks unnecessary.
Down migrations: risky — may fail on production data. Use sparingly.
Point-in-time recovery: last resort, can restore to any second.
Test PITR regularly — an untested backup is not a backup.
Always take a backup snapshot BEFORE running migrations in production.

Key Takeaways

Zero-downtime database migrations are a discipline, not a tool. The expand-contract pattern works with any migration tool (Flyway, Prisma, Liquibase, Alembic) and any database. Master the pattern, and migrations become the safest part of your deployment.

The rules: never combine schema changes and code changes in one deployment, always use CREATE INDEX CONCURRENTLY, add NOT NULL columns in two steps (add with default, then set constraint), and test every migration in staging first.

For interviews: explain expand-contract, list safe vs dangerous operations, discuss Flyway vs Prisma trade-offs, and describe how you would rename a column on a table with 100M rows without downtime.

Key Takeaways

Expand-Contract: expand schema → deploy code → contract schema.
Safe: ADD COLUMN, CREATE INDEX CONCURRENTLY, ADD CONSTRAINT NOT VALID.
Dangerous: DROP COLUMN, RENAME COLUMN, ALTER TYPE, CREATE INDEX (without CONCURRENTLY).
Flyway: versioned SQL files, Spring Boot integration, full SQL control.
Prisma Migrate: schema-first DSL, auto-generated migrations, TypeScript DX.
Always backup before migrations. Test PITR recovery regularly.
AS
Article Author
Ashutosh
Lead Developer

Related Knowledge

Tutorial

Rust for Systems Programming

5m read
Tutorial

Go Concurrency in Practice

5m read
Article

Understanding Closures in JavaScript: The Complete 2026 Guide

22 min read
Article

React 19 Server Components: The Definitive 2026 Guide

28 min read
Article

Next.js 15 App Router Masterclass: Everything You Need to Know

25 min read