Every Spring Boot project needs a database migration tool. The two serious options are Flyway and Liquibase, and teams burn real time debating them.
Here’s the short version: both work well with Spring Boot, both are autoconfigured out of the box, and both will version your schema reliably. The difference is philosophy. Flyway is SQL files with a naming convention. Liquibase is an abstraction layer over your schema changes.
That difference sounds small. It shapes everything: how you write changes, how rollback works, how multi-database support works, and how much your team has to learn.
This article walks through both tools with working config, compares them feature by feature, and covers the case that matters most for modernization work: adding a migration tool to a database that’s never had one.
Why Versioned Migrations Matter
If you’ve ever inherited a database where the schema was maintained by hand, you know the failure mode. Production has a column that staging doesn’t. Nobody knows which index got added when, or why. Deploys involve a DBA running a script from a shared drive and hoping.
A migration tool fixes this by treating schema changes like code. Every change is a file, checked into git, applied in order, and recorded in a history table in the database itself. Any environment can be brought to the current schema by running the tool. New developers get a working local database with one command.
On modernization projects this is usually step one. You can’t upgrade Spring Boot, split a monolith, or move to a new database host with confidence if you can’t reproduce the schema. Versioned migrations turn “what does production actually look like” from an investigation into a query.
Both Flyway and Liquibase do this core job well. The question is which model fits your team.
How Flyway Works with Spring Boot
Flyway’s model: you write plain SQL files, name them by version, and Flyway applies any it hasn’t seen yet. It records what ran in a flyway_schema_history table and refuses to re-run or modify applied migrations.
Add the dependency:
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-core</artifactId>
</dependency>
<!-- Since Flyway 10, database support is modular.
Add the module for your database, e.g. PostgreSQL: -->
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-database-postgresql</artifactId>
</dependency>
Spring Boot manages the version and autoconfigures Flyway against your datasource. On startup, it scans classpath:db/migration and applies pending migrations before the application finishes booting.
Configuration in application.yml:
spring:
flyway:
enabled: true
locations: classpath:db/migration
validate-on-migrate: true
# For adopting Flyway on an existing database:
# baseline-on-migrate: true
# baseline-version: 1
Migrations are SQL files with a strict naming convention: V<version>__<description>.sql. Note the double underscore.
-- src/main/resources/db/migration/V1__init.sql
CREATE TABLE customer (
id BIGSERIAL PRIMARY KEY,
email VARCHAR(255) NOT NULL UNIQUE,
full_name VARCHAR(255) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT now()
);
CREATE INDEX idx_customer_email ON customer (email);
The next change is V2__add_customer_status.sql, then V3__..., and so on. Flyway also supports repeatable migrations (R__create_views.sql) that re-run whenever their content changes, which is useful for views and stored procedures.
When SQL isn’t enough, Flyway supports Java-based migrations in the community edition. You get a JDBC connection and full programmatic control:
package db.migration;
import org.flywaydb.core.api.migration.BaseJavaMigration;
import org.flywaydb.core.api.migration.Context;
public class V4__backfill_customer_slugs extends BaseJavaMigration {
@Override
public void migrate(Context context) throws Exception {
try (var stmt = context.getConnection().createStatement()) {
// data transformation too awkward for plain SQL
stmt.execute("UPDATE customer SET slug = lower(replace(full_name, ' ', '-'))");
}
}
}
That’s the whole tool. If your team thinks in SQL, there’s nothing new to learn. The SQL you’d write anyway is the migration.
The tradeoff: your migrations are written in one database’s dialect. If you need the same migrations to run against PostgreSQL in production and something else in a test environment or a second product deployment, you’re maintaining dialect-specific SQL yourself.
How Liquibase Works with Spring Boot
Liquibase’s model: you describe changes in a changelog, and Liquibase translates them into SQL for whatever database it’s pointed at. Changelogs can be YAML, XML, JSON, or plain SQL. History lives in a DATABASECHANGELOG table, with a companion DATABASECHANGELOGLOCK table to prevent concurrent runs.
Add the dependency:
<dependency>
<groupId>org.liquibase</groupId>
<artifactId>liquibase-core</artifactId>
</dependency>
Spring Boot autoconfigures it the same way it does Flyway. Configuration in application.yml:
spring:
liquibase:
enabled: true
change-log: classpath:db/changelog/db.changelog-master.yaml
# Run only changesets tagged for this environment:
# contexts: production
The master changelog is the entry point, and it typically just includes other files:
# src/main/resources/db/changelog/db.changelog-master.yaml
databaseChangeLog:
- include:
file: db/changelog/001-init.yaml
- include:
file: db/changelog/002-add-customer-status.yaml
And a changeset file:
# src/main/resources/db/changelog/001-init.yaml
databaseChangeLog:
- changeSet:
id: 001-create-customer
author: matt
changes:
- createTable:
tableName: customer
columns:
- column:
name: id
type: bigint
autoIncrement: true
constraints:
primaryKey: true
- column:
name: email
type: varchar(255)
constraints:
nullable: false
unique: true
- column:
name: full_name
type: varchar(255)
constraints:
nullable: false
rollback:
- dropTable:
tableName: customer
More verbose than the Flyway version, clearly. But notice two things.
First, there’s no dialect in that file. Point it at PostgreSQL, MySQL, or SQL Server and Liquibase generates the appropriate DDL. If you ship software that customers run on their own database, this is a big deal.
Second, the rollback block. For many change types Liquibase can generate the rollback automatically (it knows the inverse of createTable is dropTable), and for the rest you declare it alongside the change. Rollback is a first-class concept, not an afterthought.
Liquibase also has contexts and labels for environment-specific changesets, preConditions for guarding changes, and a diff command that compares two databases or a database against your changelog. And if you’d rather write SQL, Liquibase accepts formatted SQL changelogs too, so the abstraction is optional per changeset.
Flyway vs Liquibase: Comparison
| Flyway | Liquibase | |
|---|---|---|
| Change format | SQL files (plus Java) | YAML, XML, JSON, or SQL changelogs |
| Philosophy | SQL-first, convention over abstraction | Database-agnostic change abstraction |
| Spring Boot autoconfig | Yes, spring.flyway.* | Yes, spring.liquibase.* |
| History tracking | flyway_schema_history table | DATABASECHANGELOG + lock table |
| Rollback (free edition) | No. Roll forward with a new migration | Yes, by tag or count, with auto-generated or declared rollback |
| Targeted / advanced rollback | Paid (undo migrations) | Paid (targeted rollback) |
| Dry run / SQL preview | Paid | Free (update-sql command) |
| Diff generation | Not built in | Free (diff, generate-changelog) |
| Java-based migrations | Yes, free | Yes, via custom change classes |
| Multi-database portability | Manual (per-dialect SQL) | Built in via abstraction |
| Environment targeting | Per-environment locations and placeholders | Contexts and labels |
| Adopting on existing DB | baseline | changelog-sync |
| Learning curve | Minimal if you know SQL | Changelog syntax and concepts to learn |
On paid tiers: both tools follow the same pattern. The free edition handles the core job completely, and the vendors monetize operational tooling on top: drift detection, policy checks, dry runs and undo for Flyway, targeted rollback and quality checks for Liquibase, plus support for certain older database versions. Most Spring Boot teams never hit those walls. Check the current edition matrices before you assume, because features have moved between tiers over the years.
One honest note on rollback: automatic rollback matters less in practice than it looks on a feature list. Rolling back a migration that dropped a column doesn’t bring the data back. Many teams on both tools converge on the same discipline: roll forward with a new corrective migration, and treat rollback as a tool for lower environments. If your organization has a hard requirement for scripted rollback in the release process, though, Liquibase gives you that in the free edition and Flyway doesn’t.
Migrations in CI/CD and Multiple Environments
The default Spring Boot behavior, migrations running at application startup, is fine for one service and a dev database. It gets uncomfortable in production.
The problems: your app’s startup now depends on DDL succeeding, a long-running migration can blow past your orchestrator’s health check window, and if a migration fails you have a half-started application to reason about. With multiple replicas starting at once, both tools handle locking (Flyway via database locks, Liquibase via its lock table), so you won’t get concurrent runs, but you will get replicas waiting on each other.
The pattern we see work on mature teams: run migrations as an explicit pipeline step before deploying the new application version, using the Flyway or Liquibase Maven/Gradle plugin or CLI. Then set spring.flyway.enabled: false (or the Liquibase equivalent) in production, or leave it on purely as a validation gate with nothing pending to apply.
Two rules keep multi-environment setups sane regardless of tool:
- Never edit an applied migration. Both tools checksum applied changes and will fail validation if a file changed after it ran. That error is annoying exactly once, and then it saves you repeatedly.
- Every schema change is backward compatible with the currently deployed code. Expand, migrate, contract: add the new column, deploy code that writes both, backfill, then drop the old column in a later migration. This is what makes zero-downtime deploys possible, and no tool does it for you.
For environment differences, Flyway uses placeholders and per-environment locations, while Liquibase uses contexts and labels. Liquibase’s version is more expressive. Flyway’s is harder to misuse. That tradeoff is the whole comparison in miniature.
Adopting a Migration Tool on a Legacy Database
Here’s the case that actually matters for modernization work. You’ve inherited a Spring Boot app (or a pre-Boot app you’re migrating) with a production database that has years of history and no migration tooling. You can’t start from V1__init.sql, because running CREATE TABLE against production would fail immediately.
Both tools solve this with a baseline: mark the current production schema as the starting point, and only version changes from here forward.
With Flyway, dump the current schema into V1__init.sql (so fresh environments can be built from nothing), then tell Flyway that existing databases are already at version 1:
spring:
flyway:
baseline-on-migrate: true
baseline-version: 1
On first run against production, Flyway writes a baseline row to its history table and skips everything at or below version 1. New databases run the full chain from V1. Existing ones only get V2 and up. One warning: leave baseline-on-migrate off once adoption is done, because it can mask a misconfigured datasource by silently baselining a database you didn’t intend to touch.
With Liquibase, generate a changelog from the live schema, then sync it:
# Generate a changelog that describes the current schema
# liquibase generate-changelog
# Then mark it as applied without executing it
# liquibase changelog-sync
changelog-sync writes every changeset in the changelog to DATABASECHANGELOG without running any of them. From that point the existing database and the changelog agree, and new changesets apply normally. Fresh environments just run the whole changelog for real.
Either way, the practical sequence is the same: snapshot the schema, baseline production, and enforce a hard rule that from this day forward no schema change happens outside a migration. The tooling takes an afternoon. The discipline is the part that requires management buy-in.
Which One Should You Pick
If you’re starting fresh and don’t have a forcing requirement, the honest answer is that either works and you should stop debating. But there are real signals that point one way or the other.
Pick Flyway when:
- Your team is SQL-fluent and wants migrations to be exactly the SQL that runs
- You target one database engine and expect that to stay true
- You want the smallest possible learning curve and surface area
- Roll-forward is your recovery strategy anyway, so free-edition rollback doesn’t matter
Pick Liquibase when:
- You ship to multiple database engines, or customers run your software on their own database
- Your release process requires scripted rollback without a paid license
- You want diff and changelog generation built into the free tool
- You value declarative changelogs with preconditions and per-environment contexts, and the team will actually learn them
The team dimension matters more than the feature list. Flyway’s simplicity is only a win if plain SQL covers your needs, and Liquibase’s power is only a win if the team invests in learning it. A half-understood Liquibase setup is worse than a well-run Flyway one, and vice versa.
And if you’re already using one of them: switching tools is almost never worth the disruption without a concrete driver like a new multi-database requirement. Both track history in their own metadata table, so a switch is mechanically just another baseline, but “mechanically possible” and “good use of a sprint” are different things.
When to Get Help
Adding Flyway or Liquibase to a greenfield Spring Boot app is an hour of work, and this article covers most of what you need.
The hard version is the legacy one: a production database with years of undocumented drift, multiple environments that don’t quite match, stored procedures nobody has touched since the last hiring cycle, and an application upgrade (Spring Boot 3 to 4, Java 8 to 21) happening at the same time. Baselining that safely, reconciling environment drift, and setting up a pipeline where migrations run before deploys instead of during startup takes experience with the failure modes.
We do this work regularly. If your team is untangling a legacy database as part of a broader Spring Boot or Java modernization, reach out and we can walk through your specific situation.
Related Articles
- Spring Boot 3 to 4 Migration Guide — The framework upgrade that often triggers the migration-tooling cleanup.
- Spring Data JPA Best Practices — Keep your entity layer aligned with the schema your migrations define.
- Integration Testing with Testcontainers — Run your real migrations against a real database in every test run.
- Spring Boot Consulting — Hands-on help with Spring Boot modernization projects.
Frequently Asked Questions
Does Spring Boot run Flyway or Liquibase migrations automatically?
Yes. If either tool is on the classpath and a datasource is configured, Spring Boot autoconfigures it and runs pending migrations at application startup, before the rest of the context finishes loading. You can disable this and run migrations from your CI/CD pipeline instead, which is what most teams do once they have more than one environment.
Which features of Flyway and Liquibase require a paid edition?
Flyway’s community edition covers versioned and repeatable migrations, but undo migrations, dry runs, and drift detection sit in the paid tiers. Liquibase’s open source edition includes basic rollback by tag or count, SQL preview via update-sql, and diff generation, while targeted rollback, policy checks, and advanced drift reporting are paid. Most Spring Boot teams get by fine on the free editions of either tool.
Can I introduce Flyway or Liquibase to a database that already exists?
Yes, and this is the normal case on modernization projects. Flyway handles it with a baseline: you tell it the current schema is version 1, and it only applies migrations above that. Liquibase does the same with changelog-sync, which marks a generated changelog as already applied without executing it. Either way, the existing schema becomes your starting point and every change after that is versioned.
Can I switch from Flyway to Liquibase later, or the other way around?
You can, because both tools only track history in their own metadata table and don’t own your schema. The switch is essentially the same as adopting a tool on a legacy database: baseline the current state in the new tool, stop running the old one, and drop its history table when you’re confident. It’s rarely worth doing without a concrete reason like a new multi-database requirement.
Java Modernization Readiness Assessment
15 questions your team should answer before starting a migration. Takes 10 minutes. Could save you months.