← ALL INSIGHTS
INSIGHT · 5 MIN READ

The Zero Downtime Database Migration Playbook (And How to Avoid Catastrophe).

database migration

The worst migration incident I’ve ever witnessed didn’t involve a malicious hacker, a sudden viral traffic spike, or a complex architectural collapse. It was a mundane Tuesday afternoon. A well-meaning backend engineer deployed a schema migration to add a standard foreign key index to a massive orders table. It was a single line of SQL that had executed in milliseconds in the staging environment.

But staging only had ten thousand rows. Production had 150 million.

In PostgreSQL, a standard index creation locks the table against writes until the entire operation finishes. The moment that migration ran, the database took out an exclusive lock. Every single customer trying to check out was suddenly blocked. The connection pool maxed out, application servers started throwing 500 errors, and CPU usage pegged at 100%. What was supposed to be a harmless, invisible tweak turned into a 45-minute, revenue-halting outage. The emergency fix involved frantically killing the process, but the damage was done.

Running critical schema changes on live production is high-risk work. When your application operates at scale, zero downtime is the only standard worth aiming for. Whether you are dealing with a monolith or microservices, schema evolution doesn’t have to be a ticking time bomb.

Here is the definitive database migration strategies playbook that keeps your application highly available while your data model evolves.

Expand First, Never Break

The foundational rule of modern database management is to never rewrite a table in place. Instead, you should rely on the expand and contract pattern (also known as parallel change). This pattern is the absolute backbone of any safe, highly available schema evolution.

When you need to modify a column, rename a field, or change a data type, you do not use an ALTER TABLE command to modify the existing architecture directly. That instantly breaks any running application code that expects the old schema.

Instead, Phase 1 is always expansion. You add the new column or table alongside the old one. If you are replacing an address text column with an address_id foreign key, you add address_id as a nullable column. You do not touch the existing address data. By making the new structures nullable and non-disruptive, your old application code keeps running exactly as it did before. There are no long locks, no table rewrites, and zero failed queries.

Build Indexes Concurrently

Adding indexes to large tables is a classic trap for database outages. As mentioned in the horror story above, a standard index build scans the table and blocks all INSERT, UPDATE, and DELETE operations.

If you are using PostgreSQL, the silver bullet for this problem is the create index concurrently postgres command. When you append CONCURRENTLY to your index statement, Postgres changes how it builds the data structure. It performs the build in multiple, careful phases:

  1. Catalog Entry: It flags the index as invalid in the system catalog, taking only a brief, non-blocking lock.
  2. First Scan: It scans the table to build the index while allowing normal application writes to continue unabated.
  3. Second Scan: It does a final pass to catch any new rows inserted or updated during the first scan.

It takes significantly longer to build—often two to three times as long as a standard index—but it doesn’t block your application’s write operations. Your busiest tables remain fully available. (Note: MySQL and MariaDB handle this via Online DDL natively, while Percona uses external tools like pt-online-schema-change).

Sequence Your Deploys Carefully

A successful database transition isn’t just about SQL; it’s about how you orchestrate your application deployments. A common mistake engineering teams make is trying to ship schema changes and application changes in a single, monolithic pull request.

To avoid breaking things, you must separate your deployment into distinct, isolated phases:

  • Step 1: Ship the schema expansion (e.g., adding the new nullable column to the database).
  • Step 2: Deploy the application code that writes to both the old and the new columns (Dual Writes).
  • Step 3: Backfill the historical data into the new column.
  • Step 4: Deploy the application code that shifts read operations over to the new column.
  • Step 5: Deploy the final code that stops writing to the old column entirely.

This sequencing guarantees that you maintain backward compatible schema changes at every single moment of the rollout. Old and new application versions should both be able to run safely against the same database schema simultaneously. This is critical for environments using rolling deployments or blue-green infrastructure, where two versions of the app live side-by-side for a few minutes.

Backfill Data Safely

Backfilling existing data into a newly expanded schema is often the most dangerous phase of the migration. Running a massive UPDATE my_table SET new_col = transformed(old_col) might seem incredibly efficient, but it will create massive transaction logs, consume heavy disk I/O, and lock millions of rows, effectively causing an outage.

Instead, backfill in small, manageable chunks. Write a background script that processes records in small batches (e.g., 1,000 rows at a time) with a slight delay between batches.

SQL

-- Conceptual chunked backfill loop
UPDATE users 
SET status = 'active' 
WHERE id IN (
  SELECT id FROM users 
  WHERE status IS NULL 
  LIMIT 1000
);

By throttling the backfill, you keep database locks microscopically short and ensure that production traffic always gets priority over background migration tasks. If the script fails halfway through, you haven’t rolled back a massive transaction; you just resume from exactly where the script left off.

Contract Last

The final step of the migration is the cleanup phase. Only once you are 100% certain that your application no longer reads from or writes to the old legacy column, and only after your monitoring dashboards confirm zero traffic hitting the deprecated fields, do you execute the contract phase.

Dropping an old column or removing a legacy table constraint is permanent. Wait for a “bake period”—a few days or even a week—to ensure no rogue background jobs, analytics pipelines, or cron scripts are still relying on the old schema. Once the coast is completely clear, execute the DROP COLUMN command.

The goal isn’t just a successful technical migration. It’s a migration that is so seamless, your end-users, stakeholders, and product managers never even notice it happened.

THE CONVICTION BRIEF

One brief like this, monthly.

Subscribe
FAQ

On modernizing CPG data

What does "data as a product, not a byproduct" actually mean?

It means each critical data domain gets a named owner accountable for its quality, availability, and adoption. A byproduct has no owner, no roadmap, and no service level; a product is measured by whether people use it. The shift is organizational before it is architectural.

Why start with the organization instead of the technology?

The three shifts in this piece are ownership, consumption, and governance — and none is primarily a technology decision. Companies that dominate with data made the decision before they drew the diagram. New tooling on top of unowned data just moves the same problem to a faster stack.

What's wrong with a 2015-era data stack?

Those stacks were optimized for storage and ingestion — getting data in and keeping it. Modern stacks optimize for the person pulling data out: the demand planner, the trade manager, the pricing agent. The stack that wins is the one the business actually pulls from, not the one that stores the most.

How is governance-as-enabler different from governance theater?

Governance that lives in review boards slows everything and protects little. Governance that lives in the platform — contracts, permissions, and quality gates enforced at the pipeline — speeds teams up and holds under audit. One is a meeting; the other is enforced by default.

Do we need to rebuild everything at once?

No. Start by assigning an owner to one critical domain and designing that domain for consumption, then move governance into the platform for it. The pattern is deliberate and incremental, which is why the leaders treat it as a series of shifts rather than a single migration.