Every Spring Boot project that touches a database hits this question sooner or later: Flyway or Liquibase?
I've used both across different teams and projects over the years. Both are battle-tested, both integrate with Spring Boot out of the box, and both get the job done. But I keep reaching for Flyway. Not because Liquibase is bad (it isn't), but because Flyway's philosophy aligns better with how I think about database changes.
Let me walk through the real differences, the stuff that actually matters day-to-day, and why I land where I do.
Before comparing, let's be clear about the shared goal. Both Flyway and Liquibase:
flyway_schema_history for Flyway, DATABASECHANGELOG for Liquibase)The difference is in how they approach migrations.
Flyway's approach is dead simple. You write SQL files, name them with a version prefix, and drop them in a folder. That's it.
src/main/resources/db/migration/
├── V1__create_users_table.sql
├── V2__add_email_column.sql
├── V3__create_orders_table.sql
└── V4__add_index_on_email.sql
Each file contains plain SQL:
-- V1__create_users_table.sql CREATE TABLE users ( id BIGSERIAL PRIMARY KEY, username VARCHAR(100) NOT NULL UNIQUE, created_at TIMESTAMP NOT NULL DEFAULT NOW() );
Flyway scans the folder, checks which versions have already been applied (via flyway_schema_history), and runs the new ones in order. The naming convention (V{version}__{description}.sql) is the entire configuration.
Spring Boot setup? One dependency and zero configuration:
<dependency> <groupId>org.flywaydb</groupId> <artifactId>flyway-core</artifactId> </dependency>
Spring Boot auto-detects the db/migration folder and runs Flyway on startup. You don't even need to add a single property unless you want to customize something.
Liquibase takes a different approach. Instead of raw SQL files, you define changes in a changelog file using XML, YAML, JSON, or SQL. Each change is wrapped in a changeset with an author and an ID.
Here's the same users table creation in Liquibase XML:
<!-- db/changelog/db.changelog-master.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-latest.xsd"> <changeSet id="1" author="ratheesh"> <createTable tableName="users"> <column name="id" type="BIGSERIAL" autoIncrement="true"> <constraints primaryKey="true" nullable="false"/> </column> <column name="username" type="VARCHAR(100)"> <constraints nullable="false" unique="true"/> </column> <column name="created_at" type="TIMESTAMP" defaultValueComputed="NOW()"> <constraints nullable="false"/> </column> </createTable> </changeSet> </databaseChangeLog>
Or in YAML:
databaseChangeLog: - changeSet: id: 1 author: ratheesh changes: - createTable: tableName: users columns: - column: name: id type: BIGSERIAL autoIncrement: true constraints: primaryKey: true nullable: false - column: name: username type: VARCHAR(100) constraints: nullable: false unique: true
Spring Boot setup for Liquibase:
<dependency> <groupId>org.liquibase</groupId> <artifactId>liquibase-core</artifactId> </dependency>
By default, Spring Boot looks for db/changelog/db.changelog-master.yaml (or .xml).
Let me cut through the feature-matrix noise and focus on what actually impacts your daily workflow.
This is the fundamental philosophical split.
Flyway says: "You know SQL. Write SQL."
Liquibase says: "Describe what you want, and we'll generate the right SQL for your database."
Liquibase's abstraction layer (createTable, addColumn, etc.) is genuinely useful if you deploy the same app against multiple database engines. If your app needs to run on both PostgreSQL and Oracle, writing createTable in XML is easier than maintaining two sets of SQL files.
But here's the thing: most Spring Boot projects I've worked on target one database. We pick PostgreSQL (or MySQL, or whatever) and stick with it. In that world, the abstraction layer is overhead. I'd rather write the exact CREATE TABLE statement I want, with the exact PostgreSQL-specific features I need (BIGSERIAL, JSONB columns, partial indexes), than translate my intent through XML tags.
Flyway does support Java-based migrations for complex cases, and Liquibase lets you write raw SQL changesets too. But the defaults and conventions push you in opposite directions.
This one matters more than people think.
When a teammate opens a PR with a new Flyway migration, I see SQL. I can immediately tell what's changing. I can mentally run the statement and think about locking implications, index strategies, or data type choices.
-- V12__add_status_to_orders.sql ALTER TABLE orders ADD COLUMN status VARCHAR(20) NOT NULL DEFAULT 'PENDING'; CREATE INDEX idx_orders_status ON orders(status);
That's two lines. I know exactly what's happening.
The Liquibase equivalent:
<changeSet id="12" author="ratheesh"> <addColumn tableName="orders"> <column name="status" type="VARCHAR(20)" defaultValue="PENDING"> <constraints nullable="false"/> </column> </addColumn> <createIndex indexName="idx_orders_status" tableName="orders"> <column name="status"/> </createIndex> </changeSet>
It's more verbose, and the XML ceremony makes it harder to scan quickly. In a busy PR with application code changes alongside migration changes, the Flyway version wins for me every time.
This is where Liquibase has a genuine edge, and I want to be honest about it.
Liquibase supports automatic rollback for many operations. If a createTable was applied, it knows to DROP TABLE on rollback. For operations where it can't auto-generate a rollback, you can define custom rollback SQL within the changeset.
<changeSet id="5" author="ratheesh"> <addColumn tableName="users"> <column name="phone" type="VARCHAR(20)"/> </addColumn> <rollback> <dropColumn tableName="users" columnName="phone"/> </rollback> </changeSet>
Flyway's community edition doesn't support rollback at all. The paid "Teams" edition does, but honestly? I've never relied on automated rollback in production anyway.
Here's my take: if a migration goes wrong in production, I don't want to "undo" it automatically. I want to understand what happened, assess data impact, and write a forward migration to fix it. Rolling back a CREATE TABLE is fine, but rolling back an ALTER TABLE that just ran against 50 million rows? That needs human judgment, not an automated reverse script.
So while Liquibase's rollback support is technically better, I don't consider it a dealbreaker.
Both tools have first-class Spring Boot support, but Flyway's is slightly more frictionless.
Flyway with Spring Boot:
# application.properties - honestly, you might not need ANY of these spring.flyway.enabled=true spring.flyway.locations=classpath:db/migration # That's it. Both lines above are the defaults anyway.
Liquibase with Spring Boot:
spring.liquibase.enabled=true spring.liquibase.change-log=classpath:db/changelog/db.changelog-master.yaml
The difference is minimal, but Flyway's convention of "just drop SQL files in db/migration" means there is genuinely zero configuration needed. No master changelog file to maintain, no changeset IDs to manage.
I've onboarded junior developers onto both tools. With Flyway, the conversation goes:
"Write your SQL, name the file V{next_number}__description.sql, put it in db/migration. Done."
With Liquibase, there's more to explain: changelog format choices (XML? YAML? SQL?), changeset structure, author/ID conventions, preconditions, contexts, labels. It's more powerful, but newcomers have more to absorb.
I don't want to pretend Flyway is better at everything. Liquibase has real strengths:
Multi-database support. If you ship a product that customers deploy on their choice of database (PostgreSQL, MySQL, Oracle, SQL Server), Liquibase's abstraction layer saves you from maintaining separate SQL scripts per database. Flyway can do this with location overrides, but it's more manual.
Preconditions. Liquibase can check if a table exists before trying to create it, or if a column is a certain type before altering it. This is useful for migrations that might run against databases in varying states (common in large enterprise environments).
<changeSet id="10" author="ratheesh"> <preConditions onFail="MARK_RAN"> <not><tableExists tableName="audit_log"/></not> </preConditions> <createTable tableName="audit_log"> <!-- columns --> </createTable> </changeSet>
Contexts and labels. Need to apply certain migrations only in dev but not prod? Liquibase has built-in support for this with contexts. Flyway can achieve similar results, but it's not as clean.
Diff and changelog generation. Liquibase can compare two database schemas and auto-generate a changelog of differences. This is helpful when reverse-engineering an existing database into version control for the first time.
Simplicity. There is almost nothing to learn. SQL files, naming convention, done. The cognitive overhead is close to zero.
Transparency. What you see is what's executed. No abstraction layer translating your intent into SQL behind the scenes. When something goes wrong, there's no question about what SQL actually ran.
Speed of authoring. Writing a SQL file is faster than wrapping the same logic in XML/YAML tags. Over hundreds of migrations across a project's lifetime, this adds up.
Clean Git history. Each migration is a standalone file. Git blame and log work exactly as you'd expect. With Liquibase's single-changelog-file approach (or even with includes), the history is messier.
Team convention. Since there's only one way to do things in Flyway (SQL files with version prefixes), there are fewer debates. With Liquibase, teams argue about XML vs YAML, single changelog vs multiple files, when to use preconditions, etc.
For anyone who wants to quickly try both, here's the minimum setup for each.
Flyway:
pom.xml: flyway-core dependency
(flyway-database-postgresql if using PG)
folder: src/main/resources/db/migration/
file: V1__init.sql
config: nothing required
Liquibase:
pom.xml: liquibase-core dependency
folder: src/main/resources/db/changelog/
file: db.changelog-master.yaml (+ included changelogs)
config: nothing required (Spring Boot defaults work)
Pick Liquibase when:
Pick Flyway when:
Both Flyway and Liquibase are production-ready tools used by thousands of teams. You won't regret picking either one.
But I keep choosing Flyway because it stays out of my way. I write SQL every day. I don't need an abstraction layer to write ALTER TABLE. The naming convention enforces order without any configuration. And when I review a teammate's migration PR, I see exactly what will happen to the database. No translation needed.
Liquibase is the better choice when your problem space genuinely requires its advanced features (multi-database, preconditions, policy-driven rollbacks). But for the typical Spring Boot app talking to one PostgreSQL instance? Flyway is all you need.
Pick the tool that matches your team's reality, not the one with the longest feature list.
📚 Related: If you're working with Spring Data JPA alongside your migrations, check out the Spring JPA cheat sheet and Database indexes deep dive.
Complete guide to database indexes for developers. Learn how indexes work, types of indexes, and optimization strategies for better performance.
Comprehensive guide to Spring Data JPA with practical examples and best practices. Learn how to effectively use JPA in Spring Boot applications.
Find the most popular YouTube creators in tech categories like AI, Java, JavaScript, Python, .NET, and developer conferences. Perfect for learning, inspiration, and staying updated with the best tech content.

Get instant AI-powered summaries of YouTube videos and websites. Save time while enhancing your learning experience.