Here is the translated article body:
How do you handle version control for your DB schema? With Spring Boot, Flyway is the natural pick since it’s supported out of the box, but there are also times when you want to reach for Liquibase for its rollback support and flexible syntax.
In this article, we’ll walk through integrating Liquibase into Spring Boot — from writing changelogs to executing rollbacks. We’ll also lay out the differences from Flyway so you have a basis for choosing between them.
Liquibase vs. Flyway
First, let’s compare the two to help with the adoption decision.
| Aspect | Liquibase | Flyway |
|---|---|---|
| Syntax | XML/YAML/JSON/SQL | SQL (OSS) / Java |
| Rollback | Native support | Undo is Teams/Enterprise only (OSS handles it manually via SQL) |
| Learning curve | A bit steep | Low |
| DB support | Very broad | Major DBs |
If you want to keep everything in SQL, Flyway fits well; if you want to abstract rollbacks and complex changes, Liquibase is a better match. For more on Flyway, see Managing Database Migrations with Flyway in Spring Boot.
Adding the Dependency and Initial Setup
With Maven, just add liquibase-core.
<dependency>
<groupId>org.liquibase</groupId>
<artifactId>liquibase-core</artifactId>
</dependency>
On the application.yml side, specify the changelog path and context.
spring:
liquibase:
enabled: true
change-log: classpath:db/changelog/db.changelog-master.xml
contexts: dev
If change-log is not specified, Spring Boot auto-detects classpath:/db/changelog/db.changelog-master.{yaml,yml,json,xml,sql}. When using XML, it’s reassuring to specify it explicitly.
Auto-migration is the work of LiquibaseAutoConfiguration. At application startup, Liquibase creates the DATABASECHANGELOG and DATABASECHANGELOGLOCK tables and runs any unapplied changeSets in order. To disable it, set spring.liquibase.enabled=false.
master changelog and Splitting Files
A bloated changelog quickly becomes hard to read, so split it by release or by feature. The standard layout is to have the master pull them in via include or includeAll. Match the XSD to your Liquibase version, or specify dbchangelog-latest.xsd to make it easier to keep up.
<?xml version="1.0" encoding="UTF-8"?>
<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">
<include file="db/changelog/v1.0/001-create-users.xml"/>
<include file="db/changelog/v1.0/002-create-orders.xml"/>
<includeAll path="db/changelog/v1.1/"/>
</databaseChangeLog>
includeAll loads files in alphabetical order, so control ordering with prefixes like 001-, 002-.
Basic XML changeSet Syntax
A changeSet is uniquely identified by the combination of id and author. The cardinal rule is to never change the contents of a changeSet that has already been applied.
<changeSet id="001-create-users" author="mizucoffee">
<createTable tableName="users">
<column name="id" type="BIGINT" autoIncrement="true">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="email" type="VARCHAR(255)">
<constraints nullable="false" unique="true"/>
</column>
<column name="created_at" type="TIMESTAMP" defaultValueComputed="CURRENT_TIMESTAMP"/>
</createTable>
</changeSet>
Foreign keys require the referenced table to already exist, so put them in a separate changeSet. This assumes the orders table is created first.
<changeSet id="002-create-orders" author="mizucoffee">
<createTable tableName="orders">
<column name="id" type="BIGINT" autoIncrement="true">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="user_id" type="BIGINT">
<constraints nullable="false"/>
</column>
</createTable>
</changeSet>
<changeSet id="003-fk-orders-users" author="mizucoffee">
<addForeignKeyConstraint
baseTableName="orders" baseColumnNames="user_id"
referencedTableName="users" referencedColumnNames="id"
constraintName="fk_orders_user"/>
</changeSet>
The constraints element lets you specify nullable, unique, and primaryKey. For entity design, JPA Entity Relationship Mapping is also a useful reference.
YAML and SQL Formats
If XML feels too verbose, you can write changelogs in YAML.
databaseChangeLog:
- changeSet:
id: 001-create-users
author: mizucoffee
changes:
- createTable:
tableName: users
columns:
- column:
name: id
type: BIGINT
autoIncrement: true
constraints:
primaryKey: true
nullable: false
- column:
name: email
type: VARCHAR(255)
constraints:
nullable: false
unique: true
If you already have SQL on hand, or your team is more comfortable with SQL, formatted SQL is also convenient.
--liquibase formatted sql
--changeset mizucoffee:001-create-users
CREATE TABLE users (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
email VARCHAR(255) NOT NULL UNIQUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
--rollback DROP TABLE users;
Don’t mix formats — sticking to one across the project makes reviews much easier.
Conditional Execution with preConditions and context
When you want to toggle application based on environment differences, use preConditions and context.
<changeSet id="010-seed-dev-data" author="mizucoffee" context="dev">
<preConditions onFail="MARK_RAN">
<tableExists tableName="users"/>
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM users</sqlCheck>
</preConditions>
<insert tableName="users">
<column name="email" value="[email protected]"/>
</insert>
</changeSet>
context="dev" is matched against contexts in application.yml, so it runs only in the development environment. Note that if you start without specifying contexts, the filter is disabled and every changeSet becomes a target.
The distinction is that contexts answers “which environment to run in,” while labels answers “which tagged group of changes to run.” Use contexts for environment separation, and labels when you want to bundle changes like feature flags — splitting them this way keeps things tidy.
Writing Rollbacks
Liquibase’s biggest appeal is rollback support. Reversible changes like createTable get their reverse operation derived automatically, but for data changes or sql tags you write it manually.
<changeSet id="020-add-status-column" author="mizucoffee">
<addColumn tableName="users">
<column name="status" type="VARCHAR(20)" defaultValue="ACTIVE"/>
</addColumn>
<rollback>
<dropColumn tableName="users" columnName="status"/>
</rollback>
</changeSet>
<changeSet id="021-tag-v1" author="mizucoffee">
<tagDatabase tag="v1.0.0"/>
</changeSet>
If you set checkpoints with tagDatabase, you can later roll back to that tag. Here’s an example of running it via the Maven plugin.
# Roll back the most recent change
mvn liquibase:rollback -Dliquibase.rollbackCount=1
# Roll back to a specific tag
mvn liquibase:rollback -Dliquibase.rollbackTag=v1.0.0
For destructive DROP-style operations (the state after dropTable or delete), even with a rollback tag the data itself can’t be restored. Remember: the schema can be reverted, but the contents cannot. If you anticipate rollbacks, consider logical deletes or moving data to a separate table.
Per-Profile Operation
The simplest way to control behavior per environment is to switch via application-{profile}.yml.
# application-prod.yml
spring:
liquibase:
enabled: false # In production, migrations are run explicitly from CI
contexts: prod
When enabled: false is set for production, run it explicitly from CI/CD like this.
mvn liquibase:update -Dliquibase.contexts=prod
For more on switching profiles, see Safely Switching Environment-Specific Configuration with Profiles.
Common Pitfalls
Here’s a quick summary of points that tend to trip people up right after adoption.
- Checksum mismatch error: Occurs when you edit an already-applied changeSet. As a rule, don’t edit them — add a new corrective changeSet instead. If you absolutely must, run
mvn liquibase:clearCheckSumsto recompute them. - DATABASECHANGELOGLOCK won’t release: If a lock is left behind by a forced shutdown or similar, release it with
mvn liquibase:releaseLocksor UPDATE the table directly. - changelog not loaded: Check the
classpath:path and file placement (src/main/resources/db/changelog/). Also confirm thatincludeAllisn’t pointing at an empty directory. - Coexisting with Hibernate
ddl-auto: Combining it withupdateand similar settings can cause unexpected schema changes, so in production it’s safest to set it tononeand consolidate on Liquibase.
Summary
Liquibase’s strengths are its flexible syntax across XML/YAML/SQL and its standard rollback support. Choose between Flyway and Liquibase based on your team’s skill set and the complexity of your changes.
Start small with a single changeSet, and secure release-level rollback points with tagDatabase — operations will get significantly easier.