Flyway & Liquibase
Flyway and Liquibase are the two most common JVM-based migration runners for PostgreSQL in enterprise pipelines. Both track applied changes, support CI, and integrate with Spring and release orchestration.
Search across all documentation pages
Flyway and Liquibase are the two most common JVM-based migration runners for PostgreSQL in enterprise pipelines. Both track applied changes, support CI, and integrate with Spring and release orchestration.
# Flyway
flyway -url=jdbc:postgresql://localhost:5432/myapp \
-user=app_migrator -password="$DB_PASS" \
-locations=filesystem:db/migration \
migrate
# Liquibase
liquibase --url=jdbc:postgresql://localhost:5432/myapp \
--username=app_migrator --password="$DB_PASS" \
--changeLogFile=db/changelog/db.changelog-master.yaml \
updateWhen to reach for this: Multiple deploy targets, audit requirements, checksum validation, and teams already standardized on JVM tooling.
Flyway layout:
db/migration/
V1__baseline.sql
V2__add_orders.sql
V3__orders_index_concurrent.sql # note: run outside txn - see docs
-- V2__add_orders.sql
SET ROLE app_owner;
CREATE TABLE app.orders (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
total_cents integer NOT NULL
);
RESET ROLE;Liquibase master changelog:
# db/changelog/db.changelog-master.yaml
databaseChangeLog:
- include:
file: changes/001-baseline.sql
relativeToChangelogFile: true
- include:
file: changes/002-orders.yaml
relativeToChangelogFile: true# changes/002-orders.yaml
databaseChangeLog:
- changeSet:
id: 002-orders
author: data-team
changes:
- sqlFile:
path: 002-orders.sql
relativeToChangelogFile: true
rollback:
- sql: DROP TABLE IF EXISTS app.orders;What this demonstrates:
V{n}__ naming| Feature | Flyway | Liquibase |
|---|---|---|
| Primary format | SQL files | YAML/XML/SQL/JSON |
| Rollback | Undo scripts (Teams) / forward-fix | Built-in rollback sections |
| Checksums | Yes | Yes |
| Concurrent index story | Manual split migrations | Same - SQL file with runInTransaction: false |
| Learning curve | Lower for SQL-first teams | Higher, more flexible |
# flyway.conf
flyway.postgresql.transactional.lock=false
flyway.cleanDisabled=true# Liquibase changeSet for CONCURRENT index
- changeSet:
id: idx-concurrent
runInTransaction: false
changes:
- sql:
sql: CREATE INDEX CONCURRENTLY idx_orders_created ON app.orders (created_at);CREATE INDEX CONCURRENTLY cannot run in a transaction.clean in production Flyway - it drops all objects.lock_timeout and statement_timeout in migration SQL (see safety rules).flyway validate # checksum drift detection
flyway info # pending vs applied
liquibase status
liquibase validateV4__fix.sql, never rewrite V2.executeInTransaction=false per migration (Flyway) or runInTransaction: false (Liquibase).app_migrator with SET ROLE app_owner.| Alternative | Use When | Don't Use When |
|---|---|---|
| Sqitch | Perl shop, explicit deploy/revert | Team wants zero CLI deps |
| Rails/Prisma/Alembic | App framework owns schema | DBA mandates raw SQL git repo |
| Hand-run psql | Never in prod | You need audit trail |
Flyway when the team is SQL-first. Liquibase when you need richer changelog metadata and rollback DSL in enterprise change boards.
Separate repeatable migrations (R__seed.sql in Flyway) or non-prod-only pipelines - not versioned prod DDL files mixed casually.
Flyway baseline or Liquibase changelog-sync - one-time operation with written ADR and backup.
Stack versions: This page was written for PostgreSQL 18.4 (stable 18, maintenance 17), pgvector 0.8+, PgBouncer 1.x, Patroni 3.x, and PostGIS 3.5+.
Reviewed by Chris St. John·Last updated Jul 19, 2026