Database Version Control: Flyway and Liquibase Guide
In this tutorial, you'll learn about Database Version Control: Flyway and Liquibase Guide. We cover key concepts, practical examples, and best practices to help you understand and apply this topic effectively.
Database version control is the practice of managing database schema changes through version-controlled Migration scripts -- ensuring that database schemas evolve consistently across environments, can be rolled back when needed, and are auditable through a complete change history.
What You'll Learn
You will implement Flyway and Liquibase for schema versioning, integrate migrations into CI/CD pipelines, manage rollback strategies, handle merge conflicts, and adopt zero-downtime Migration patterns for production deployments.
Why Database Version Control Matters
Schema changes deployed without versioning cause drift between environments and difficult-to-debug failures. Doda Browser deploys database changes alongside application code; using Flyway eliminated schema drift incidents, which previously caused 3-4 production incidents per quarter.
Version Control Learning Path
flowchart LR A[Database Design] --> B[Migration Tools] B --> C[Database Version Control] C --> D[CI/CD Integration] C:::current classDef current fill:#f90,color:#fff,stroke:#333,stroke-width:2px
Prerequisites: Familiarity with database migrations and basic SQL DDL. Understanding of PostgreSQL or MySQL.
Tool Comparison
| Feature | Flyway | Liquibase | Alembic | golang-migrate |
|---|---|---|---|---|
| File format | SQL | XML/YAML/JSON/SQL | Python | SQL |
| Rollback | Paid only | Built-in | Built-in | Built-in |
| CI/CD friendly | Excellent | Good | Good | Good |
| Repeatable migrations | Yes | Yes | No | No |
| Java-based | Yes | Yes | No | No |
| Undo support | Flyway Teams | Built-in | downgrade() | Down Migration |
Flyway in Practice
Project Structure
db/
migrations/
V1__create_users.sql
V2__add_orders.sql
V3__add_last_login.sql
V4__create_indexes.sql
repeatable/
R__seed_lookup_tables.sql
R__grant_permissions.sql
Migration Files
-- V1__create_users.sql
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
email VARCHAR(255) NOT NULL UNIQUE,
password_hash VARCHAR(255) NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
CREATE INDEX idx_users_email ON users (email);
-- V2__add_orders.sql
CREATE TABLE orders (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES users(id),
total DECIMAL(12,2) NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'pending',
shipping_address TEXT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
CREATE INDEX idx_orders_user_id ON orders (user_id);
CREATE INDEX idx_orders_status ON orders (status);
Configuration
# flyway.toml (Flyway 10+)
[environments.default]
url = "jdbc:postgresql://localhost:5432/mydb"
user = "app_user"
password = "${DB_PASSWORD}"
schemas = ["public"]
[flyway]
locations = ["filesystem:db/migrations", "filesystem:db/repeatable"]
baselineOnMigrate = true
baselineVersion = 0
CI/CD Integration
# GitHub Actions - Flyway migration step
jobs:
migrate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run Flyway migrations
run: |
docker run --rm \
-v ${{ github.workspace }}/db:/flyway/project/db \
-e FLYWAY_URL=jdbc:postgresql://${{ secrets.DB_HOST }}:5432/${{ secrets.DB_NAME }} \
-e FLYWAY_USER=${{ secrets.DB_USER }} \
-e FLYWAY_PASSWORD=${{ secrets.DB_PASSWORD }} \
flyway/flyway:10 migrate
Liquibase in Practice
Changelog Structure
<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.20.xsd">
<!-- Include individual changelogs -->
<include file="changelogs/001-create-users.xml"/>
<include file="changelogs/002-create-orders.xml"/>
<include file="changelogs/003-add-indexes.sql"/>
</databaseChangeLog>
Changeset with Rollback
<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.20.xsd">
<changeSet id="1" author="dodatech">
<createTable tableName="users">
<column name="id" type="bigint" autoIncrement="true">
<constraints primaryKey="true"/>
</column>
<column name="name" type="varchar(100)">
<constraints nullable="false"/>
</column>
<column name="email" type="varchar(255)">
<constraints nullable="false" unique="true"/>
</column>
</createTable>
<rollback>
<dropTable tableName="users"/>
</rollback>
</changeSet>
<changeSet id="2" author="dodatech">
<addColumn tableName="users">
<column name="last_login" type="timestamp"/>
</addColumn>
<rollback>
<dropColumn tableName="users" columnName="last_login"/>
</rollback>
</changeSet>
</databaseChangeLog>
Liquibase Commands
# Deploy pending changesets
liquibase --changeLogFile=db/changelog-master.xml update
# Rollback last 2 changesets
liquibase --changeLogFile=db/changelog-master.xml rollbackCount 2
# Rollback to specific date
liquibase --changeLogFile=db/changelog-master.xml rollbackToDate 2026-06-20
# Generate diff between databases
liquibase --changeLogFile=db/changelog-master.xml diff \
--referenceUrl=jdbc:postgresql://prod-host/proddb
Merge Conflicts in Migrations
When multiple developers create migrations with the same version number.
Prevention
# Require unique migration versions in CI
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Check for duplicate migration versions
run: |
db/migrations/*.sql | grep -oP 'V\d+' | sort | uniq -d
if [ $? -eq 0 ]; then
echo "Duplicate migration versions found!"
exit 1
fi
Resolution Strategy
-- Developer A creates V3__add_profile.sql
-- Developer B creates V3__add_settings.sql (conflict!)
-- Resolution: Rename one to V4 and reorder
-- V3__add_profile.sql stays as-is
-- V4__add_settings.sql (renamed, depends on V3 only if needed)
Repeatable Migrations
Run every time their checksum changes. Useful for views, functions, and grants.
-- R__create_user_view.sql
CREATE OR REPLACE VIEW active_users AS
SELECT id, name, email, created_at
FROM users
WHERE deleted_at IS NULL;
-- R__grant_permissions.sql
GRANT SELECT ON ALL TABLES IN SCHEMA public TO readonly_role;
GRANT SELECT, INSERT, UPDATE ON ALL TABLES IN SCHEMA public TO readwrite_role;
Zero-Downtime Migration with Version Control
-- V5__add_preferred_name.sql
-- Step 1: Add column as nullable (no downtime)
ALTER TABLE users ADD COLUMN preferred_name VARCHAR(100);
-- V6__backfill_preferred_name.sql
-- Step 2: Backfill in batches (run separately, may take hours)
DO $$
DECLARE
batch_size INT := 10000;
BEGIN
LOOP
UPDATE users
SET preferred_name = name
WHERE preferred_name IS NULL
AND ctid IN (
SELECT ctid FROM users
WHERE preferred_name IS NULL
LIMIT batch_size FOR UPDATE SKIP LOCKED
);
EXIT WHEN NOT FOUND;
COMMIT;
END LOOP;
END $$;
-- V7__make_preferred_name_not_null.sql
-- Step 3: Add NOT NULL constraint (run after backfill)
ALTER TABLE users ALTER COLUMN preferred_name SET NOT NULL;
Common Version Control Errors
1. Editing a Migration After It Has Been Applied
Flyway detects checksum changes and refuses to apply. Never edit applied migrations. Create a new Migration to fix issues.
2. No Rollback Scripts
Without rollback scripts, reverting a deployment requires manual intervention. Every Migration should have a corresponding rollback.
3. Running Migrations Automatically on Application Startup
Auto-Migration on startup causes problems with multiple instances (race conditions, duplicate migrations). Use a separate Migration step in CI/CD.
4. Ignoring Migration Order Dependencies
If V5 depends on data created in V4, they must run in order. Test the entire Migration chain from scratch on a fresh database.
5. Not Testing Migrations on Production-Scale Data
A Migration that takes 2 seconds on a development database may take 2 hours on production. Test with production-sized datasets in staging.
6. No Locking Strategy for Concurrent Migrations
Two deployment pipelines running migrations simultaneously can corrupt the schema history. Use database-level locks or CI/CD environment checks.
7. Storing Secrets in Migration Files
Passwords, API keys, and other secrets in Migration files are exposed in version control. Use environment variables or secrets management.
Practice Questions
1. What is the difference between versioned and repeatable migrations in Flyway?
Versioned migrations (V1, V2) run once in order. Repeatable migrations (R__) run every time their checksum changes, useful for views and grants.
2. How does Liquibase handle rollbacks?
Each changeset can include a <rollback> section defining how to revert the change. Liquibase also supports automatic rollback generation for simple operations.
3. How do you handle Migration conflicts in a team?
Use CI to check for duplicate version numbers. Communicate before creating migrations. Rename conflicting migrations and reorder.
4. What should you do if a Migration fails in production?
Stop the deployment, assess the failure (partial apply vs no apply), run the rollback script, fix the Migration, re-test, and re-deploy.
5. Challenge: Set up a database version control pipeline.
Design a pipeline for a team of 5 developers deploying to staging and production. Answer: (1) Use Flyway with SQL migrations. (2) CI checks for duplicate versions and validates SQL syntax. (3) Migrate test database from scratch in CI to verify full chain. (4) Deploy to staging first, run integration tests. (5) Deploy to production as a separate step. (6) Rollback script for every Migration. (7) Monitor flyway_schema_history for failures.
FAQ
Try It Yourself
Set up Flyway for a project:
- Install Flyway or use the Docker image
- Create V1__create_users.sql with users table
- Run
flyway migrateand verify the table was created - Create V2__add_orders.sql and apply it
- Check
flyway_schema_historytable - Add a rollback script for V2
- Run
flyway migrateand verify version tracking
What's Next
You have learned database version control with Flyway and Liquibase, CI/CD integration, rollback strategies, and merge Conflict Resolution. Start by adding the Flyway dependency to your project and converting your current schema into V1.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro