Course topics

By WebNest Studio Engineering Team

Database Design Tutorial

Migration Strategy for Evolving Database Schemas

A migration is a controlled, scripted change to a database schema — adding a column, creating a table, changing a constraint — applied in a specific order so that every environment (a developer's machine, staging, production) ends up with the exact same schema history. Because a production database keeps running and serving traffic while a schema evolves, migration strategy is really about answering one question: how do you change the shape of the data underneath a live application without breaking it?

The core discipline is favoring additive, backward-compatible changes over destructive ones, and sequencing any genuinely breaking change across multiple deploys so the application and the database are never out of sync in a way that causes errors.

Why it matters

A live database cannot be dropped and recreated; its structure has to change while the application keeps running and the data stays safe. Renaming a column in one step breaks every running copy of the code that still uses the old name. Staged, reversible migrations are how teams change schemas without downtime.

Additive Changes First

Adding a new nullable column, adding a new table, or adding a new index are all changes that old application code can safely ignore — they don't break anything currently running. This is why the safe default in schema evolution is to prefer additive changes: introduce the new column or table alongside the old structure, deploy application code that can use it, and only remove or rename the old structure in a later, separate migration once nothing depends on it anymore.

Handling Genuinely Breaking Changes Safely

Some changes are inherently breaking — renaming a column, making a nullable column NOT NULL, dropping a column still read by running code. These need to be split into safe stages instead of a single destructive migration: for a rename, add the new column, backfill it from the old one, deploy application code that writes to both and reads from the new one, then in a later migration drop the old column once no code references it. This staged approach means at every single point in the rollout, both the old and new application code can run against the current schema without errors — important because deployments are rarely instantaneous across every server.

  • Safe/additive: add nullable column, add table, add index, add a new constraint that existing data already satisfies.
  • Needs staging: rename column/table, change a column's type, make a column NOT NULL, drop a column or table.
  • Rule of thumb: a migration is safe if the previous version of the application still runs correctly against the new schema.

Up and Down Migrations, and Migration Tools

Tools like Flyway, Liquibase, and Prisma Migrate manage migrations as an ordered, version-controlled sequence of scripts, each one recorded in a migrations table inside the database itself so the tool always knows exactly which migrations have already run. Each migration conceptually has an "up" (apply the change) and often a "down" (reverse it) — though in practice, rolling forward with a new corrective migration is usually safer in production than rolling back, since a "down" migration can destroy data that was written after the "up" ran. Migrations should be small, single-purpose, and never edited after they have run in any shared environment; a mistake is fixed with a new migration, not by rewriting history.

Example

A safe, staged rename of customers.name to customers.full_name across three migrations

SQL
-- Migration 001: add the new column, additive and safe
ALTER TABLE customers ADD COLUMN full_name VARCHAR(150);

-- Migration 002: backfill existing rows, then have the app write to both
-- columns during the transition (application code change, not shown)
UPDATE customers SET full_name = name WHERE full_name IS NULL;
ALTER TABLE customers ALTER COLUMN full_name SET NOT NULL;

-- Migration 003 (run only after all app instances read/write full_name
-- exclusively, confirmed by deploy, monitoring, and time elapsed):
ALTER TABLE customers DROP COLUMN name;
Output
At every point between these three migrations, both the old application
code (still using "name") and the new application code (using
"full_name") can run without errors, because migration 001 only adds a
column and migration 002 populates it without touching "name". Only
migration 003, run well after the rollout is confirmed complete, removes
the old column.

Common Mistakes

  • Renaming or dropping a column in a single migration deployed at the same time as the application code change, causing errors for any server still running the old code during the rollout window.
  • Editing or deleting an already-applied migration file instead of writing a new corrective migration, which desyncs the migration history across environments.
  • Treating a "down" migration as a safe undo button in production, when it can silently discard real data written after the "up" migration ran.
  • Making a bulk data change and a schema structure change in the same migration script, making it hard to reason about or roll forward from if either half fails.

Exercise

Rename customers.name to full_name in the safe, staged way, as three separate steps: add the new column; copy the existing values into it; and, once nothing uses the old column any more, drop it. Then read the new column.

Expected output: full_name then Asha Rao

SQL · starter code
CREATE TABLE customers (id INTEGER PRIMARY KEY, name TEXT);
INSERT INTO customers (id, name) VALUES (1, 'Asha Rao');

-- TODO: migration 1: add the column full_name

-- TODO: migration 2: backfill full_name from name

-- TODO: migration 3 (after the application has switched over): drop the column name

SELECT full_name FROM customers;
Show hints
  • Columns are added and removed with ALTER TABLE ... ADD COLUMN and ALTER TABLE ... DROP COLUMN.
  • The backfill is an UPDATE that sets one column from the other.
Show solution
SQL · solution
CREATE TABLE customers (id INTEGER PRIMARY KEY, name TEXT);
INSERT INTO customers (id, name) VALUES (1, 'Asha Rao');

ALTER TABLE customers ADD COLUMN full_name TEXT;

UPDATE customers SET full_name = name;

ALTER TABLE customers DROP COLUMN name;

SELECT full_name FROM customers;

Quick quiz

1. Which change is safe to deploy without coordinating with the application?

  • A. Dropping a column
  • B. Renaming a table
  • C. Adding a new nullable column
  • D. Changing a column's type
Show answer

C. Existing code does not know about the new column and is unaffected by it.

2. What is a "down" migration?

  • A. The script that reverses a migration
  • B. A migration that failed
  • C. A migration that deletes the database
  • D. A migration run on a slow server
Show answer

A. It allows a deployment to be rolled back.

3. Why is renaming a column in a single step risky on a live system?

  • A. The rename is slow
  • B. The data is lost
  • C. It breaks the indexes
  • D. Application code still running with the old name fails immediately
Show answer

D. During a deployment, old and new versions of the code run at the same time.

Interview questions

How do you make a breaking schema change without downtime?

Show answer

With the expand-and-contract pattern. Expand: add the new column or table alongside the old one. Deploy code that writes to both and backfill the existing rows. Switch reads to the new structure. Contract: once no deployed code uses the old structure, remove it in a later migration. Each step is compatible with the code versions running at that moment, so nothing breaks at any point.

What is a migration tool and why use one?

Show answer

A migration tool, such as Flyway, Liquibase, Alembic or the one built into a framework, keeps schema changes as ordered, versioned files in the code repository and records in the database which have been applied. Every environment can then be brought to the same schema by running the pending migrations in order. Changes are reviewed like code, are repeatable, and can be reversed.

Key Points to Remember

  • Prefer additive, backward-compatible migrations (new nullable columns, new tables) so both old and new application code keep working during a rollout.
  • Genuinely breaking changes (renames, NOT NULL, drops) should be staged across multiple migrations and deploys, never done in one destructive step.
  • Migration tools (Flyway, Liquibase, Prisma Migrate) track applied migrations in the database itself, in strict order, so every environment converges on the same schema.
  • Fix a mistake with a new forward migration rather than editing or rolling back an already-applied one in a shared environment.

Practice in the playground

Write and run your own code for this topic, or start from the exercise or an example. Runs your SQL on SQLite in your browser. Standard SQL works; a few features that exist only in PostgreSQL or MySQL do not. Your code for each topic is kept in this browser.

Run the examples

Change an input, predict the result, then compare it with the output. Explain why the result changes.

These examples need a project environment on your computer, so they can be copied but not run here. Codelab runs Python, FastAPI backends with SQLite, plain Java 17 and HTML/CSS/JavaScript.

WebNest Studio

Written by

WebNest Studio Engineering Team

Practical lessons from developers working with Java, Python, React, databases and production systems.

Led by Vansh Duggal, Founder, WebNest Studio · Full-stack & software engineering