Database2 September 2025·10 min read

Database Migration Best Practices for Production Systems

Zero-downtime migrations, versioned migrations with Drizzle and Prisma, rollback strategies, data backfill patterns, and testing migrations safely.

DatabaseMigrationsDrizzlePrismaPostgreSQLZero Downtime

Why Database Migrations Are Scary

Database migrations in production are terrifying because they are irreversible in ways that code deployments are not. You can roll back a bad code deployment in seconds with a container image swap. Rolling back a database migration that dropped a column, transformed data types, or restructured tables is dramatically harder — sometimes impossible without data loss.

Yet migrations are inevitable. Business requirements evolve, schemas need to change, and data models that were perfect six months ago need updating. The question is not whether to migrate — it is how to migrate safely.

Zero-Downtime Migration Principles

The golden rule: never make a breaking change in a single deployment. Instead, split every breaking migration into multiple non-breaking steps:

Adding a Column

This is the simplest migration. Add the column with a default value or allow NULL. Deploy code that writes to the new column. Backfill existing rows. Deploy code that reads from the new column. This can be done in a single deployment since adding a nullable column does not break existing queries.

Removing a Column

This requires two deployments:

1. Deploy 1: Remove all code references to the column. The column still exists in the database but nothing reads from or writes to it.

2. Deploy 2: Run the migration to drop the column.

If you drop the column first, any running server instance that references it will throw errors during the deployment window.

Renaming a Column

Never rename directly. Instead:

1. Add the new column

2. Deploy code that writes to both old and new columns

3. Backfill the new column from the old column

4. Deploy code that reads from the new column

5. Deploy code that stops writing to the old column

6. Drop the old column

Yes, this is six steps for a rename. That is the cost of zero downtime.

Versioned Migrations with Modern ORMs

Drizzle ORM

Drizzle Kit generates SQL migration files from your schema changes. Run drizzle-kit generate to create a numbered migration file. Run drizzle-kit migrate to apply pending migrations. Each migration is tracked in a drizzle migrations table.

Drizzle's approach is SQL-first — you can inspect and modify the generated SQL before applying it. This is valuable for production systems where you need to review exactly what will run.

Prisma

Prisma Migrate generates migrations from changes to your schema.prisma file. Run npx prisma migrate dev to create and apply migrations in development. Run npx prisma migrate deploy in production to apply pending migrations without generating new ones.

Prisma tracks migrations in a _prisma_migrations table and prevents drift between your schema file and the database.

Rollback Strategies

Every migration should have a corresponding rollback plan:

Additive migrations: (adding columns, tables, indexes): Rollback by dropping the added objects. Low risk.
Destructive migrations: (dropping columns, changing types): Take a logical backup before the migration. Document the exact rollback SQL.
Data transformations: Store the original data in a temporary column or table until you confirm the migration succeeded. Drop the backup after a verification period.

Automated Rollback Scripts

For every migration file, create a corresponding down migration. Drizzle and Prisma both support this pattern. In CI/CD, test that applying and then rolling back a migration leaves the database in its original state.

Data Backfill Patterns

Large table backfills can lock rows and degrade performance. Use batched updates:

Process 1,000-10,000 rows per batch
Add a short delay between batches to reduce database load
Use a cursor-based approach (WHERE id > last_processed_id) rather than OFFSET for consistent performance
Log progress so you can resume if the backfill is interrupted

Testing Migrations

Test against a production-like dataset: Migrations that work on 100 rows may timeout on 10 million rows.
Measure lock duration: Use pg_stat_activity to monitor locks during migration testing.
Run in a staging environment first: Always. No exceptions.
Test the rollback: If you have never tested your rollback, you do not have a rollback.

Database migrations do not have to be scary. They just have to be planned. Want help with a complex migration? Reach out.

BH

The Beyond Horizon Team

We are a digital agency based in Ajmer, India, specializing in Next.js web applications, React Native mobile apps, and UI/UX design. 150+ projects delivered.

About Us →

Have a project in mind?

We build fast, SEO-ready web and mobile applications.

Get a Free Consultation