Managing Database Versioning with Liquibase in a Spring Boot Application

As applications evolve, so does the database. New features require new tables, existing columns need modifications, indexes must be optimized, and relationships become more complex. Managing these schema changes manually — especially across multiple environments — can quickly become error-prone and chaotic.

This is where Liquibase becomes essential.

Liquibase is an open-source database version control tool that allows developers to define, track, and automate schema changes in a structured and reliable way. Instead of manually executing SQL scripts in different environments, Liquibase ensures that every change is versioned, traceable, and consistently applied.

In this guide, I’ll walk you through integrating Liquibase into a Spring Boot application, so database schema changes can be tracked, versioned, and applied automatically.

Prerequisites

  • List the required tools and software:
    • Spring Boot Application
    • Mysql Server

Add Liquibase dependency

Add the following dependency in the pom.xml file

<dependency>
    <groupId>org.liquibase</groupId>
    <artifactId>liquibase-core</artifactId>
</dependency>

pom.xml

Configure SpringLiquibase bean

Create a configuration class named LiquibaseConfig.java and add following content in it

import javax.sql.DataSource;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import liquibase.integration.spring.SpringLiquibase;

@Configuration
public class LiquibaseConfig {
	
  @Autowired
  private DataSource dataSource;
      
  @Bean
  public SpringLiquibase springLiquibase() {
      SpringLiquibase liquibase = new SpringLiquibase();
      
      liquibase.setDataSource(dataSource);
      liquibase.setChangeLog("db/changelog/master.changelog.xml");
      liquibase.setDatabaseChangeLogTable("database_change_log");
      liquibase.setDatabaseChangeLogLockTable("database_change_log_lock");
      
      return liquibase;
  }

}

LiquibaseConfig.java

This class is a Spring Boot configuration that sets up Liquibase for the application. It injects the DataSource to provide database connectivity and creates a SpringLiquibase bean pointing to the master changelog file. The bean also customizes the change log and lock table names to track executed changeSets and manage concurrency. On application startup, Liquibase automatically executes any pending migrations, ensuring the database schema is always up-to-date.

Disable auto schema generation/update by JPA/Hibernate

We will set hibernate.hbm2ddl.auto to validate so database only updated by liquibase and the ORM just ensures the validation of schema with the entity classes.

Creating Liquibase-Related Files

Once the prerequisites are ready, the next step is to create the files and folders that Liquibase uses to manage database migrations. Liquibase organizes changes into changelog files, which are version-controlled and executed in a structured order. Following a clear folder structure is important for maintainability and collaboration.

Create Directory Structure

Start by creating a dedicated folder inside your Spring Boot project to store all Liquibase files:

src/main/resources/db/changelog/

changelog

Why this folder?

  • src/main/resources is the standard location in Spring Boot for configuration files, ensuring Liquibase can load the files from the classpath.
  • db/changelog keeps all your database migration scripts organized in one place.
  • Having a separate folder for database changes avoids mixing them with other resource files, making version control cleaner and easier to maintain.

Create the Master Changelog File

Create a file named master.changelog.xml in the changelog directory. Then add the following content in it

<?xml version="1.1" encoding="UTF-8" standalone="no"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
   xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext"
   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
   xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog-ext 
   http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-ext.xsd 
   http://www.liquibase.org/xml/ns/dbchangelog 
   https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd"
   logicalFilePath="path-independent">

<!-- tables -->
<include relativeToChangelogFile="true" file="./tables/create_table.changelog.xml" ></include>

<!-- seeded data -->
<include relativeToChangelogFile="true" file="./seed_data/insert_seeded_data.sql" context="sample-app-seed"></include>

<!-- migration scripts -->
<include relativeToChangelogFile="true" file="./migration/migration_scripts.changelog.xml" ></include>
     
</databaseChangeLog>

master.changelog.xml

Here's an explanation of each section in master.changelog.xml

<include... file="./tables/create_table.changelog.xml" >

  • Includes the table creation changelog file located in the tables folder.

<include... file="./seed_data/insert_seeded_data.sql" context="sample-app-seed">

  • Includes a seed data SQL file that runs only when the sample-app-seed context is enabled.

<include file="./migration/migration_scripts.changelog.xml">

  • Includes migration-related changelog files for ongoing database updates.

Create the table Changelog File

Next, we will create a folder named tables in db/changelog and create a file named create_table.changelog.xmlin it. Then add the following content in it

<?xml version="1.1" encoding="UTF-8" standalone="no"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
   xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext"
   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
   xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog-ext 
   http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-ext.xsd 
   http://www.liquibase.org/xml/ns/dbchangelog 
   https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd"
   logicalFilePath="path-independent">

<include relativeToChangelogFile="true" file="./user.sql" />

</databaseChangeLog>

create_table.changelog.xml

Here's an explanation of create_table.changelog.xml

<include relativeToChangelogFile="true" file="./user.sql">

  • Includes the user.sql file from the same directory as the current changelog file so Liquibase can execute the SQL statements defined inside it.
  • You can include create-table scripts for all the tables in your application here, and they will be executed in the exact order in which they are listed in the XML file.

Create the table DDL script

we will create a file named user.sqlin the tables folder, then add the following content in it

-- liquibase formatted sql
-- changeset git-username:create_user logicalFilePath:path-independent
  CREATE TABLE IF NOT EXISTS `user` (
    `id` bigint(20) NOT NULL AUTO_INCREMENT,
    `first_name` varchar(255) COLLATE utf8_bin NOT NULL,
    `last_name` varchar(255) COLLATE utf8_bin DEFAULT NULL,
    PRIMARY KEY (`id`)
  );
-- rollback DROP TABLE if exists user;

user.sql

Here's an explanation of user.sql

– liquibase formatted sql

  • Tells Liquibase that this file uses Liquibase formatted SQL syntax.

– changeset git-username:create_user logicalFilePath:path-independent

  • Defines a unique changeSet with author git-username and ID create_user, with a logical path to avoid environment-specific path issues.

CREATE TABLE IF....);

  • Sql query to create the table

– rollback DROP TABLE if exists user;

  • Defines the rollback operation to drop the user table if this changeSet needs to be reverted.

Create the seed data File

A seed data file can be used to automatically populate the database with essential initial or reference data required for the application to function correctly. Using Liquibase, seed data becomes version-controlled and environment-aware.

We will create a folder named seed_data in db/changelog and create a file named insert_seeded_data.sql in it. Then add the following content in it

-- liquibase formatted sql
-- changeset git-username:sample-app-seed runOnChange:True; logicalFilePath:path-independent

SET FOREIGN_KEY_CHECKS = 0; 
TRUNCATE table user;
INSERT INTO `user`(`id`,`first_name`,`last_name`) VALUES (1,'Admin','User');
SET FOREIGN_KEY_CHECKS = 1;

insert_seeded_data.sql

Here's an explanation of insert_seeded_data.sql

– changeset... runOnChange:True;

  • means that Liquibase will re-run the changeSet automatically whenever the changeSet file is modified, even if it has already been executed.

SET FOREIGN_KEY........

  • Actual insert statements, in this case, we are creating one user in the database.

Handling future Change in schema

Up to this point, we have configured the part where new tables will be created by Liquibase and seed data will be automatically populated. But what if we need to alter an existing table, add new columns, change constraints, or update indexes? This is where Liquibase’s incremental migration feature comes into play.

Instead of modifying already executed changeSets, we create new changeSets for every schema modification. With a changeSet that describes this alteration, liquibase will detect that it is a new change and apply it safely without touching previous migrations.

To configure this we will create a folder named migration and migration/2026 and in db/changelog and create a file named migration_scripts.changelog.xml in it. Then add the following content in it

<?xml version="1.1" encoding="UTF-8" standalone="no"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
   xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext"
   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
   xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog-ext 
   http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-ext.xsd 
   http://www.liquibase.org/xml/ns/dbchangelog 
   https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd"
   logicalFilePath="path-independent">
                   
  <includeAll relativeToChangelogFile="true" path="2026/"/>
	
</databaseChangeLog>

migration_scripts.changelog.xml

Here's an explanation of migration_scripts.changelog.xml

<includeAll relativeToChangelogFile="true" path="2026/"/>

  • This tells Liquibase to include all changelog files located in the 2026/ directory, relative to the current changelog file. This is useful for organizing migrations by year, feature, or module, keeping the master changelog cleaner and more maintainable.

Create an alter table script

As an exercise we will create an alter script to add date_of_birth column to the user table, you would create a new file like add_date_of_birth_column_to_user.sql in 2026

-- liquibase formatted sql

-- changeset git-username:alter_user_add_date_of_birth_column logicalFilePath:path-independent
ALTER TABLE user ADD COLUMN `date_of_birth` DATETIME NULL;
-- rollback ALTER TABLE user DROP COLUMN date_of_birth;

add_date_of_birth_column_to_user.sql

Run the application

With Liquibase configured, your Spring Boot application is now ready to deploy. First, configure the MySQL database connection in application.yml

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/sample_app_db
    username: root
    password: your_password
  jpa:
    hibernate:
      ddl-auto: validate

application.yml

When you run the application, Liquibase will automatically create the database schema and prepopulate it with the seed data defined in your changelogs.

You can then access your APIs and verify that the tables and initial data have been correctly set up in MySQL.

Conclusion

Implementing Liquibase in your Spring Boot application transforms how you handle database changes — bringing your database schema under version control just like application code. With a modular changelog structure, contextual seed data, and automated execution on startup, Liquibase ensures schema consistency, safer deployments, and easier collaboration across teams. From initial table creation to incremental migrations and data seeding, your database is now fully managed, reliable, and synchronized with application releases, making your app truly ready for production.

For a complete working example of this setup, including all changelog files and configuration, check out the GitHub repository here

GitHub - navalgandhi1989/java-liquibase
Contribute to navalgandhi1989/java-liquibase development by creating an account on GitHub.