Flyway vs Liquibase: Choosing the Right Database Migration Tool for Java Projects

May 16, 2026

Picture this: your team just agreed on the database schema for a new microservice. Three sprints later, half the developers have outdated local databases, production is on version 7 while staging is on version 5, and someone just dropped a column that another feature depends on. The “it works on my machine” chorus starts playing, and you realize you need a proper database migration strategy — yesterday.

I’ve been in that situation more times than I care to admit. After running database migrations across dozens of Java projects over the years, I’ve developed a clear preference for specific scenarios. In this post, I’ll break down the two dominant tools in the Java ecosystem — Flyway and Liquibase — so you can make an informed choice instead of copying whatever was in the last project’s pom.xml.

Why Database Migrations Matter

Before comparing tools, let’s align on the problem. Database migrations are version-controlled scripts that evolve your schema over time. Without them, you have:

  • Inconsistent environments — dev, staging, and production drift apart
  • No rollback path — when a deployment goes wrong, you’re manually reverting changes
  • Team conflicts — developers stepping on each other’s schema changes
  • Deployment anxiety — “Will this ALTER TABLE break production?”

A good migration tool turns schema changes into repeatable, reviewable, automated processes. Both Flyway and Liquibase solve this, but they approach it differently.

Flyway: The SQL-First Purist

Flyway, now owned by Redgate, is the tool I reach for when I want migrations to feel like plain SQL with superpowers.

How It Works

Flyway uses a simple convention: place SQL files in src/main/resources/db/migration/ with a specific naming pattern:

V1__create_users_table.sql
V2__add_email_index.sql
V3__create_orders_table.sql

The V{number}__{description}.sql format tells Flyway the execution order. It tracks applied migrations in a flyway_schema_history table and only runs new ones.

A Practical Example

Here’s a typical Flyway migration for a Spring Boot project:

-- V1__create_users_table.sql
CREATE TABLE users (
    id BIGSERIAL PRIMARY KEY,
    email VARCHAR(255) NOT NULL UNIQUE,
    name VARCHAR(100) NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE INDEX idx_users_email ON users(email);
-- V2__add_user_preferences.sql
ALTER TABLE users ADD COLUMN preferences JSONB;

CREATE TABLE user_sessions (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    expires_at TIMESTAMP NOT NULL
);

In your application.properties:

spring.flyway.enabled=true
spring.flyway.locations=classpath:db/migration
spring.flyway.baseline-on-migrate=true

And in pom.xml:

<dependency>
    <groupId>org.flywaydb</groupId>
    <artifactId>flyway-core</artifactId>
</dependency>

Spring Boot auto-configures Flyway on startup. Your application won’t start if migrations fail, which is exactly the safety net you want.

Flyway’s Strengths

1. Simplicity That Sticks

There’s almost nothing to learn. If you know SQL, you know Flyway. No XML, no YAML, no custom DSL. This makes code reviews straightforward — any DBA can read and approve a .sql file without learning a new format.

2. Repeatable Migrations (R scripts)

For data that changes frequently (reference data, lookup tables), Flyway supports R__ scripts that run every time they change:

-- R__seed_user_roles.sql
INSERT INTO roles (name, permissions) VALUES
    ('admin', '{"users": "write", "orders": "write"}'),
    ('user', '{"users": "read", "orders": "read"}')
ON CONFLICT (name) DO UPDATE SET permissions = EXCLUDED.permissions;

3. Java-Based Migrations

When SQL isn’t enough for complex data transformations, you can write migrations in Java:

public class V4__MigrateUserData extends BaseJavaMigration {
    @Override
    public void migrate(Context context) throws Exception {
        try (Statement stmt = context.getConnection().createStatement()) {
            ResultSet rs = stmt.executeQuery("SELECT id, legacy_data FROM users");
            while (rs.next()) {
                long id = rs.getLong("id");
                String legacyData = rs.getString("legacy_data");
                String jsonData = convertToJson(legacyData);
                // Update with new format
            }
        }
    }
}

4. Clean Undo Support (Paid)

Flyway Teams edition supports U__ undo migrations, but the open-source version does not. This is a significant limitation I’ll address later.

Liquibase: The Flexible Generalist

Liquibase, originally from Datical and now part of Perforce, takes a fundamentally different approach. Instead of raw SQL files, you describe changes in XML, YAML, JSON, or SQL “changelog” files.

How It Works

Liquibase uses a master changelog file that references individual changesets:

# db.changelog-master.yaml
databaseChangeLog:
  - include:
      file: db/changelog/001-create-users.yaml
  - include:
      file: db/changelog/002-add-orders.yaml

Each changeset is an atomic change with metadata:

# db/changelog/001-create-users.yaml
databaseChangeLog:
  - changeSet:
      id: 001-create-users
      author: fernando.nogueira
      changes:
        - createTable:
            tableName: users
            columns:
              - column:
                  name: id
                  type: BIGSERIAL
                  autoIncrement: true
                  constraints:
                    primaryKey: true
              - column:
                  name: email
                  type: VARCHAR(255)
                  constraints:
                    nullable: false
                    unique: true
              - column:
                  name: created_at
                  type: TIMESTAMP
                  defaultValueComputed: CURRENT_TIMESTAMP

Liquibase’s Strengths

1. Database Abstraction

Liquibase’s XML/YAML format is database-agnostic. The same changelog can generate different SQL for PostgreSQL, MySQL, Oracle, or SQL Server:

<changeSet id="001" author="fernando">
    <createTable tableName="users">
        <column name="id" type="${bigint.type}" autoIncrement="true">
            <constraints primaryKey="true"/>
        </column>
    </createTable>
</changeSet>

The ${bigint.type} resolves to BIGSERIAL on Postgres, BIGINT AUTO_INCREMENT on MySQL, etc. This is invaluable if you support multiple database vendors.

2. Built-In Rollback

Every changeset can declare its reverse operation:

- changeSet:
    id: 002-add-preferences
    author: fernando.nogueira
    changes:
      - addColumn:
          tableName: users
          columns:
            - column:
                name: preferences
                type: JSONB
    rollback:
      - dropColumn:
          tableName: users
          columnName: preferences

Run liquibase rollbackCount 1 and the column is gone. This is available in the free open-source version, which is a massive advantage over Flyway Community.

3. Pre-Conditions and Contexts

Liquibase can conditionally run changes based on the environment:

- changeSet:
    id: 003-seed-dev-data
    author: fernando.nogueira
    context: dev
    preConditions:
      - onFail: MARK_RAN
      - sqlCheck:
          expectedResult: 0
          sql: SELECT COUNT(*) FROM users
    changes:
      - insert:
          tableName: users
          columns:
            - column: { name: email, value: "test@example.com" }
            - column: { name: name, value: "Test User" }

This changeset only runs in dev context when the users table is empty. No more accidental seed data in production.

4. Diff and Generate ChangeLog

Liquibase can compare two databases and generate the changelog automatically:

$ liquibase --url=jdbc:postgresql://dev:5432/mydb \
  --referenceUrl=jdbc:postgresql://prod:5432/mydb \
  diffChangeLog

This is a lifesaver when you need to sync schemas between environments or reverse-engineer an existing database.

Head-to-Head Comparison

Feature Flyway Liquibase
Migration format SQL files (native syntax) XML, YAML, JSON, or SQL
Learning curve Low — just SQL Medium — XML/YAML DSL
Database abstraction None — raw SQL per database High — generate SQL per platform
Rollback (free) ❌ Not available ✅ Built-in
Rollback (paid) ✅ Flyway Teams ✅ Already included
Repeatable migrations R__ scripts runOnChange changesets
Java migrations BaseJavaMigration CustomTaskChange
Pre-conditions ❌ Not available preConditions tag
Context/environment filtering ❌ Limited context attribute
Generate changelog from DB ❌ Not available generateChangeLog
Spring Boot support ✅ Auto-configured ✅ Auto-configured
Micronaut support ✅ Plugin available ✅ Plugin available
IDE support ✅ Good ✅ Better (Visual Studio Code extension)
Commercial backing Redgate (Flyway Teams $$$) Perforce (Liquibase Pro $$$)
Community/open-source ✅ Active ✅ Active
CI/CD integration ✅ Maven, Gradle, CLI ✅ Maven, Gradle, CLI, GitHub Actions

When I Choose Flyway

I reach for Flyway in these scenarios:

1. PostgreSQL-Only Projects

When I’m building a service that will only ever run on Postgres, database abstraction is irrelevant. Raw SQL gives me full access to Postgres-specific features: JSONB, ARRAY, GIN indexes, CITEXT, custom types, and extensions like PostGIS or pgvector.

2. Teams with Strong SQL Skills

If my team writes SQL confidently and reviews schema changes like code, Flyway removes an abstraction layer they don’t need. DBAs can read the migrations without learning Liquibase XML.

3. Simple CRUD Services

For microservices with straightforward schemas (10-20 tables, no complex relationships), Liquibase’s flexibility is overkill. Flyway’s convention-based approach gets out of the way.

4. When Rollback Isn’t Critical

In many modern deployment pipelines, rollback means restoring from backup or blue-green deployment, not reversing migrations. If your ops strategy doesn’t depend on migration rollbacks, Flyway’s limitation here doesn’t matter.

When I Choose Liquibase

I switch to Liquibase for these situations:

1. Multi-Database Support

When the application must run on PostgreSQL, MySQL, Oracle, or SQL Server depending on the customer. Liquibase’s abstraction layer prevents duplicating migrations per database.

2. Complex Schema Evolution

For enterprise applications with 100+ tables, complex relationships, and frequent refactoring. Liquibase’s pre-conditions, contexts, and change tracking provide the control you need.

3. Rollback-Required Deployments

When the ops team insists on liquibase rollback as part of the deployment strategy. This is common in regulated industries where every change must be reversible.

4. Brownfield Projects

When inheriting a database with no migration history. Liquibase’s generateChangeLog creates a baseline from the existing schema, then you add new changesets. Flyway has no equivalent feature.

5. Non-SQL Teams

When the team prefers YAML/XML over SQL, or when migrations are maintained by application developers who aren’t comfortable writing database-specific DDL.

Spring Boot Integration Comparison

Both tools integrate cleanly with Spring Boot, but the experience differs:

Flyway with Spring Boot

<dependency>
    <groupId>org.flywaydb</groupId>
    <artifactId>flyway-core</artifactId>
</dependency>
spring.flyway.enabled=true
spring.flyway.baseline-on-migrate=true
spring.flyway.validate-on-migrate=true

That’s it. Migrations run on application startup. Failures prevent startup. No XML configuration needed.

Liquibase with Spring Boot

<dependency>
    <groupId>org.liquibase</groupId>
    <artifactId>liquibase-core</artifactId>
</dependency>
spring.liquibase.change-log=classpath:db/changelog/db.changelog-master.yaml
spring.liquibase.contexts=dev

You need to create the master changelog file, but Spring Boot handles execution.

Micronaut Integration

Both tools work with Micronaut through their respective plugins:

Flyway:

flyway:
  datasources:
    default:
      enabled: true
      clean-disabled: true

Liquibase:

liquibase:
  datasources:
    default:
      change-log: classpath:db/liquibase-changelog.xml

Micronaut’s startup speed means migrations run quickly, and both tools integrate without friction.

My Hybrid Approach

On a recent project, I used both tools for different purposes — and it worked well:

  • Flyway for the main application schema (clean SQL, team prefers it)
  • Liquibase for a reporting module that needed to support PostgreSQL and MySQL

This is overkill for most projects, but it illustrates that the tools aren’t mutually exclusive. Choose the right tool for the specific problem.

Common Pitfalls with Both Tools

1. Modifying Applied Migrations

Never edit a migration that has already run in production. Both tools checksum migrations and will fail if a applied script changes. Always create a new migration to fix or alter previous changes.

2. Long-Running Migrations in Transactions

For large tables, ALTER TABLE can lock the table for minutes. Both tools run migrations in transactions by default, which can cause downtime. For zero-downtime deployments, use online schema change tools (like pt-online-schema-change or pg_repack) alongside your migration tool.

3. Missing Baseline on Existing Databases

When introducing Flyway or Liquibase to a project with an existing database, always create a baseline migration that represents the current schema. Otherwise, the tool will try to create tables that already exist.

4. Not Testing Migrations in CI

Your CI pipeline should apply migrations to a test database and verify the application starts. Both Maven and Gradle plugins support this:

$ ./mvnw flyway:migrate
$ ./mvnw spring-boot:run
$ ./mvnw liquibase:update
$ ./mvnw spring-boot:run

Performance and Scale Considerations

Aspect Flyway Liquibase
Migration execution speed Faster — minimal overhead Slightly slower — XML/YAML parsing
Startup impact Negligible for < 50 migrations Negligible for < 50 migrations
Large schema support (500+ tables) Works, but management becomes manual Better — includeAll and modular changelogs
Concurrent migration safety ✅ Locking via flyway_schema_history ✅ Locking via DATABASECHANGELOGLOCK

For most Java applications, performance differences are irrelevant. Both tools are fast enough that migration time is measured in milliseconds, not seconds.

Conclusion: My Recommendations

After years of using both tools, here’s my decision framework:

Choose Flyway when:

  • You’re on PostgreSQL (or a single database) and plan to stay there
  • Your team is comfortable with SQL
  • You want the simplest possible setup
  • Rollback via migration reversal isn’t required
  • You’re building a microservice with a focused schema

Choose Liquibase when:

  • You need to support multiple database vendors
  • You need built-in rollback capabilities
  • The schema is complex and evolves frequently
  • You’re working with a brownfield database
  • Your team prefers YAML/XML over raw SQL
  • You need pre-conditions, contexts, or environment-specific migrations

The honest truth: for 80% of Java projects I work on, Flyway is sufficient and simpler. I switch to Liquibase when the project complexity justifies the additional abstraction layer.

Both tools are mature, actively maintained, and well-supported in the Java ecosystem. The “wrong” choice is still better than no migration tool at all. Pick one, standardize on it, and focus on building features instead of arguing about DDL.

References


Profile picture

Written by Fernando Nogueira a Brazilian software engineer who lives and works remotely from Spain.