Short senior-level answer (what to say first)
I test migrations in isolation and in realistic environments.
I run them locally against a clean database, against a database with real historical data, and in CI using ephemeral databases. I also validate rollback behavior, performance on large tables, and backward compatibility with the running application. For risky migrations, I use dry runs, shadow tables, or multi-step migrations.
That’s the headline. Now let’s break it down.
1. Local testing (developer machine)
Goals
- Catch syntax errors
- Validate constraints, indexes, FK behavior
- Check basic correctness
Typical flow
- Start a fresh DB (Docker)
- Apply all migrations from scratch
- Start the application and smoke-test
Example:
docker-compose up postgres
./gradlew flywayMigrate
What I always check:
- Migration order
- Idempotency (re-run safety if tool supports it)
- FKs & indexes created as expected
⚠️ Common mistake:
“I only run migrations on my existing local DB.”
That misses cold-start failures.
2. Testing on real-like data (the most important part)
Why this matters
Most migration bugs appear only with:
- Large tables
- Existing NULLs
- Broken historical data
- Unexpected constraints violations
What I do
- Take a sanitized dump from staging / prod
- Restore locally
- Run migrations on top of it
pg_restore -d app_db staging_dump.dump
./gradlew flywayMigrate
Things I validate:
- Migration duration
- Locks (
ALTER TABLEis dangerous) - Index creation time
- Unexpected data failures
💡 Interview tip:
Always mention real data, not only empty schemas.
3. CI testing (automated)
Standard setup
- Ephemeral DB (Docker / Testcontainers)
- Migrate from zero
- Run tests
Example:
- PostgreSQL container
- Flyway / Liquibase
- Integration tests depend on migration success
What CI catches:
- Broken SQL
- Missing dependencies
- Order issues
What CI does not catch:
- Performance problems on large tables
4. Backward compatibility testing (very senior signal)
This is where many candidates fail.
Question you must answer:
Can the old application version work with the new schema?
Typical techniques:
- Expand → Migrate → Contract
- Add nullable column
- Deploy app writing both
- Backfill data
- Remove old column later
- Avoid:
- Dropping columns immediately
- Making NOT NULL without defaults
- Renaming columns without compatibility
If you mention this → interviewer knows you’ve done production work.
5. Rollback strategy (trick question)
Truthful senior answer:
I don’t rely on automatic rollback.
I prefer forward-only migrations and compensate with new migrations.
Still, I test:
- Can I restore from backup?
- Can I deploy a hotfix migration?
- Are migrations repeatable / checksum-safe?
Mentioning backups > rollback scripts is a strong signal.
6. Performance & locking checks (Postgres example)
Before production:
- Check execution plan
- Check lock type
- Avoid long
ACCESS EXCLUSIVElocks
Techniques:
CREATE INDEX CONCURRENTLY- Chunked updates
- Background backfills
Example:
EXPLAIN ANALYZE
UPDATE users SET status = 'ACTIVE';
7. Staging / pre-prod validation
Final gate before prod:
- Same DB engine & version
- Same migration tool
- Same deployment pipeline
What I verify:
- Migration time window
- Monitoring alerts
- Ability to stop rollout
Perfect interview answer (polished, 30–40 seconds)
I test migrations at multiple levels.
Locally, I apply all migrations from scratch to catch ordering and syntax issues. Then I run them against a sanitized copy of real production data to catch data and performance problems. In CI, migrations run automatically on ephemeral databases.
Before production, I validate backward compatibility and avoid destructive changes by using multi-step migrations. I don’t rely on rollbacks; instead, I prepare forward fixes and always ensure backups exist.