Why Schema Version Control Matters
Application code lives in Git — every change is tracked, reviewed, and reversible. Database schema changes often don't get the same treatment. Ad-hoc ALTER TABLE commands applied manually leave databases in unknown states across environments. Schema version control tools fix this.
Flyway vs Liquibase: Quick Comparison
| Feature | Flyway | Liquibase |
|---|---|---|
| Changelog format | SQL or Java migrations | SQL, XML, YAML, JSON |
| Rollback | Manual undo scripts (Community); auto in Teams/Enterprise | Built-in rollback support |
| Learning curve | Simple, convention-based | More complex, more features |
| Database support | All major databases | All major databases |
| Best for | Teams wanting simplicity | Teams needing advanced rollback and multi-database |
Flyway in Practice
-- Migrations are SQL files named with versions:
-- V1__Create_orders_table.sql
-- V2__Add_status_index.sql
-- V3__Add_shipping_address.sql
-- V3__Add_shipping_address.sql
ALTER TABLE orders
ADD COLUMN shipping_address_line1 VARCHAR(255),
ADD COLUMN shipping_address_line2 VARCHAR(255),
ADD COLUMN shipping_city VARCHAR(100),
ADD COLUMN shipping_zip VARCHAR(20);-- Flyway tracks applied migrations in flyway_schema_history
SELECT installed_rank, version, description, type, installed_on, success
FROM flyway_schema_history
ORDER BY installed_rank;# CI/CD: apply pending migrations
flyway -url=jdbc:postgresql://localhost/mydb \
-user=myapp -password=${DB_PASS} \
migrate
# Validate that migrations haven't been changed
flyway validate
# See what's pending
flyway infoLiquibase in Practice
# changelog.yaml
databaseChangeLog:
- changeSet:
id: 3
author: alice
changes:
- addColumn:
tableName: orders
columns:
- column:
name: shipping_address_line1
type: VARCHAR(255)
rollback:
- dropColumn:
tableName: orders
columnName: shipping_address_line1# Apply changes
liquibase update
# Roll back last 1 changeset
liquibase rollback-count 1
# Roll back to a tag
liquibase tag pre-v2.0
liquibase rollback pre-v2.0Zero-Downtime Migration Patterns
The expand-contract pattern enables schema changes without downtime:
-- Phase 1 (Expand): Add new column, keep old
ALTER TABLE users ADD COLUMN email_normalized VARCHAR(255);
-- Deploy app that writes to BOTH old and new columns
-- Phase 2 (Backfill): Populate new column
UPDATE users
SET email_normalized = LOWER(TRIM(email))
WHERE email_normalized IS NULL
LIMIT 10000; -- Run in batches
-- Phase 3 (Switch): App reads from new column only
-- Phase 4 (Contract): Drop old column
ALTER TABLE users DROP COLUMN email;CI/CD Integration
# GitHub Actions workflow
jobs:
migrate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run Flyway migrations
uses: flyway/flyway-github-action@v1
with:
url: jdbc:postgresql://${{ secrets.DB_HOST }}/${{ secrets.DB_NAME }}
user: ${{ secrets.DB_USER }}
password: ${{ secrets.DB_PASS }}
command: migrateTeam Workflow Best Practices
- Never modify an applied migration — create a new one instead. Both tools validate checksums and will fail if you modify history.
- Test rollbacks in staging before production for every migration
- Use repeatable migrations (R__ prefix in Flyway) for views, stored procedures, and functions
- Review migrations in PR as carefully as application code
- Tag database state before major releases for easy rollback points
Key Takeaways
- Flyway is simpler; Liquibase has better built-in rollback support
- Never modify applied migrations — always add new changesets
- Use expand-contract for zero-downtime schema changes
- Integrate schema migrations into CI/CD alongside application deployments
- Test rollback procedures in staging before every production deployment