Flyway vs Liquibase for PostgreSQL: An Honest Comparison
Flyway and Liquibase are the two most widely used database migration tools in the Java ecosystem. Both support PostgreSQL. Both are mature. And both have significant differences in philosophy, syntax, and capabilities. This guide compares them honestly, including where each one falls short.
Philosophy: Convention vs Configuration
Flyway follows a convention-over-configuration approach. Migrations are plain SQL files with a naming convention (V1__description.sql). There is one way to do things, and it is simple. If you know SQL, you know Flyway.
Liquibase takes a configuration-heavy approach. Migrations (called "changesets") can be written in XML, YAML, JSON, or SQL. Liquibase provides a database-agnostic abstraction layer with its own DSL for schema changes. This gives more flexibility at the cost of complexity.
Migration Syntax
Flyway: Plain SQL
-- V2__add_users_email_index.sql
-- Flyway migration: just SQL with a naming convention
CREATE INDEX CONCURRENTLY idx_users_email ON users (email);
-- That's it. The filename determines the version and description.
-- V2 = version 2
-- add_users_email_index = description (underscores become spaces)Liquibase: XML/YAML/SQL changesets
<!-- db.changelog-2.xml -->
<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.0.xsd">
<changeSet id="2" author="dev">
<createIndex indexName="idx_users_email"
tableName="users"
unique="false">
<column name="email"/>
</createIndex>
</changeSet>
</databaseChangeLog>Or in YAML:
# db.changelog-2.yaml
databaseChangeLog:
- changeSet:
id: 2
author: dev
changes:
- createIndex:
indexName: idx_users_email
tableName: users
columns:
- column:
name: emailOr as raw SQL (Liquibase also supports this):
-- db.changelog-2.sql
-- liquibase formatted sql
-- changeset dev:2
CREATE INDEX CONCURRENTLY idx_users_email ON users (email);Feature Comparison
| Feature | Flyway | Liquibase |
|---|---|---|
| Migration format | SQL, Java | XML, YAML, JSON, SQL |
| PostgreSQL support | Excellent | Excellent |
| Rollback support | Paid (Teams) | Free (auto + manual) |
| Schema diff | No | Yes (diff command) |
| Database-agnostic DSL | No (SQL is DB-specific) | Yes (XML/YAML changesets) |
| Conditional execution | No | Yes (preconditions) |
| Transaction control | Per-migration only | Per-changeset |
| Learning curve | Low | Medium-High |
| Community edition | Apache 2.0 | Apache 2.0 |
| SQL safety linting | No | No |
Rollback Strategies
Flyway
Flyway's free tier does not support rollbacks at all. You can only move forward. The Teams/Enterprise tier adds "undo migrations" (files named U2__description.sql) that you write manually. There is no auto-generated rollback.
-- V2__add_status_column.sql
ALTER TABLE users ADD COLUMN status TEXT DEFAULT 'active';
-- U2__add_status_column.sql (Flyway Teams only)
ALTER TABLE users DROP COLUMN status;Liquibase
Liquibase has built-in rollback support in the free tier. For XML/YAML changesets, Liquibase auto-generates rollback SQL for many operations (CREATE TABLE, ADD COLUMN, etc.). For complex or SQL-based changesets, you specify rollback manually:
<changeSet id="2" author="dev">
<addColumn tableName="users">
<column name="status" type="TEXT" defaultValue="active"/>
</addColumn>
<!-- Rollback auto-generated: DROP COLUMN status -->
</changeSet>
<!-- For SQL changesets, specify rollback explicitly -->
<changeSet id="3" author="dev">
<sql>
CREATE INDEX CONCURRENTLY idx_users_email ON users (email);
</sql>
<rollback>
DROP INDEX CONCURRENTLY idx_users_email;
</rollback>
</changeSet>PostgreSQL-Specific Considerations
Transaction handling
Both Flyway and Liquibase wrap migrations in transactions by default. This is generally good for atomicity but causes problems with specific PostgreSQL operations:
CREATE INDEX CONCURRENTLYcannot run inside a transactionALTER TYPE ... ADD VALUEcannot run inside a transaction (PG 11 and earlier)DROP INDEX CONCURRENTLYcannot run inside a transaction
-- Flyway: Disable transaction for specific migration
-- Add this comment at the top of the file:
-- flyway:executeInTransaction=false
CREATE INDEX CONCURRENTLY idx_users_email ON users (email);
-- Liquibase: Disable transaction per changeset
<changeSet id="2" author="dev" runInTransaction="false">
<sql>
CREATE INDEX CONCURRENTLY idx_users_email ON users (email);
</sql>
</changeSet>Liquibase abstraction layer limitations
Liquibase's database-agnostic DSL does not support many PostgreSQL-specific features:
- No CONCURRENTLY support in the XML
<createIndex>tag - No NOT VALID support for constraints
- No support for partitioning (PARTITION BY)
- No support for RLS policies
- Limited enum type support
For any PostgreSQL-specific migration, you end up using raw SQL changesets anyway, which negates the benefit of the abstraction layer. If you are only targeting PostgreSQL, the XML/YAML format adds complexity without benefit.
When to Choose Flyway
- You want simplicity. Flyway's SQL-first approach means there is almost nothing to learn. Write SQL, name the file correctly, done.
- You only target PostgreSQL. No need for a database-agnostic abstraction layer.
- Your team knows SQL well. Flyway encourages SQL fluency rather than hiding behind an abstraction.
- You want minimal tooling overhead. Fewer config files, fewer concepts, fewer surprises.
When to Choose Liquibase
- You need multi-database support. If your application targets PostgreSQL, MySQL, and Oracle, the abstraction layer helps.
- You need rollback in the free tier. Flyway's rollback is a paid feature.
- You need conditional execution. Liquibase preconditions let you run changesets conditionally (e.g., only if a table does not exist).
- You need schema diff. Liquibase can compare two databases and generate the diff as changesets.
- You have complex deployment workflows. Liquibase's changeset model with contexts and labels provides more granular control.
What Neither Tool Does: SQL Safety Analysis
Both Flyway and Liquibase are migration runners. They track which migrations have been applied, execute them in order, and handle versioning. What neither tool does is analyze the SQL itself for safety.
Neither Flyway nor Liquibase will warn you about:
- CREATE INDEX without CONCURRENTLY (blocks writes)
- Missing lock_timeout (can queue all traffic)
- ALTER COLUMN TYPE causing a full table rewrite
- SET NOT NULL without the CHECK pattern (ACCESS EXCLUSIVE scan)
- Volatile defaults that rewrite the table
- Foreign keys that lock both tables
These tools apply your migrations. They do not tell you whether those migrations are safe to run against a production database. This is a fundamentally different concern, and it is where migration linting tools come in.
Combining Migration Runners with Safety Linting
The best approach is to use both: a migration runner (Flyway or Liquibase) for execution, and a linter for safety analysis. They complement each other:
# Example CI pipeline combining Flyway + MigrationPilot
# Step 1: Lint the migration SQL for safety issues
npx migrationpilot check src/main/resources/db/migration/*.sql \
--fail-on critical
# Step 2: If lint passes, run the migration with Flyway
flyway migrateOr with Liquibase:
# For Liquibase SQL changesets:
npx migrationpilot check src/main/resources/db/changelog/*.sql \
--fail-on critical
# For Liquibase XML/YAML, generate SQL first:
liquibase update-sql > /tmp/pending.sql
npx migrationpilot analyze /tmp/pending.sqlMigrationPilot auto-detects both Flyway and Liquibase (plus 12 other frameworks) and works with any migration runner. It analyzes the SQL for lock safety, table rewrites, and 80 other patterns — the exact gap that migration runners do not fill. It runs in CI as a GitHub Action, GitLab CI step, or plain CLI command.
Summary
- Flyway: Simple, SQL-first, great for PostgreSQL-only projects. Rollback is paid.
- Liquibase: Flexible, multi-format, rollback included. More complex, abstraction layer has PostgreSQL gaps.
- Both: Run migrations but do not analyze SQL for safety. Pair with a linter for production-safe migrations.
- For PostgreSQL-only projects, Flyway's simplicity usually wins.
- For multi-database environments or when you need free rollback, Liquibase is the better choice.