Application code has Git. We commit changes, track history, handle branches, and revert broken releases seamlessly.
What about Database schemas??? Historically, they have been managed through manual SQL scripts passed around in messaging channels or executed directly in production—hoping nothing breaks.
Liquibase
Liquibase brings Git-like version control, tracking, and rollbacks directly to your database schema management.
1. Why Liquibase is Needed
Consider a realistic setup with multiple deployment environments: Dev, QA, and PreProd.
Each environment moves at a different pace:
- PreProd has 5 SQL scripts applied (Version 1.0).
- QA is ahead with 7 SQL scripts applied (Version 2.0—2 scripts ahead of PreProd).
- Dev (the developer playground) has 10 SQL scripts applied (Version 3.0—3 scripts ahead of QA and 5 ahead of PreProd).
[PreProd] --> Scripts 1..5 (v1.0)
[QA] --> Scripts 1..7 (v2.0)
[Dev] --> Scripts 1..10 (v3.0)
Now, imagine the Product Owner requests an immediate feature removal due to a pivot. One specific SQL script in QA or PreProd must be reverted or dropped.
Without Liquibase, how would you handle this?
- Direct Database Manipulation? You cannot simply SSH in or open SQL Server Management Studio with superuser credentials to drop tables. Enterprise governance prohibits direct production/pre-production database access for developers.
- Deploy PreProd to Production early? What if PreProd is not ready for a production release yet?
This is where Liquibase rescues the workflow: it allows you to cleanly roll back specific scripts in any target environment via automated pipelines without needing direct database access.
2. Why Not Flyway or Native SQL Server?
Why Not Flyway Community?
Flyway is a popular alternative, but there is a major catch: Flyway's Community Edition does not support automated rollbacks (flyway undo). To get rollbacks in Flyway, you must purchase their enterprise tier.
Liquibase supports rollbacks out of the box in its open-source / community version.
Why Not Just Run Manual Queries on SQL Server?
In small personal projects or low-risk environments, logging in directly to modify tables works. In enterprise settings, this approach fails because:
- Security & Auditing: Giving individual developers DDL access creates compliance risks (SOC2, HIPAA, ISO).
- Human Error: Manual queries lack checksum verification, leading to environment drift.
- Pipeline Automation: Enterprises require every standard script to have an accompanying rollback script so DevOps pipelines can execute changes safely and automatically.
Because Liquibase provides open-source rollbacks, every database change can be tested, applied, and reverted safely in CI/CD.
3. How Liquibase Tracks State Under the Hood
When Liquibase executes against a database, it maintains two core tracking tables:
-
DATABASECHANGELOG: Logs every applied changeset (id, author, filename, dateexecuted, md5sum). -
DATABASECHANGELOGLOCK: A boolean lock mechanism that prevents multi-pod deployment conflicts in Kubernetes or microservice instances.
Keeping SQL scripts separate from environment-specific configurations keeps your repository clean, modular, and easy to navigate across quarters and releases.
Here is a clean, production-tested folder layout:
Liquibase-db/
├── master-changelog.xml # Core entry point linking all releases
├── environments/ # DB connection properties per environment
│ ├── DEV.properties
│ ├── QA.properties
│ └── PREPROD.properties
├── release-1.0/ # Q1 / Release 1.0 scripts
│ ├── CREATE_SCRIPTS_1.0.sql
│ ├── CREATE_SCRIPTS_1.0_ROLLBACK.sql
│ ├── INSERT_SCRIPTS_1.0.sql
│ └── INSERT_SCRIPTS_1.0_ROLLBACK.sql
└── release-2.0/ # Q2 / Release 2.0 scripts
├── CREATE_NEW_TABLE_2.0.sql
├── CREATE_NEW_TABLE_2.0_ROLLBACK.sql
├── UPDATE_SCRIPT_2.0.sql
└── UPDATE_SCRIPT_2.0_ROLLBACK.sql
4. Environment-Specific Execution in Large Enterprises
In large corporate environments, you often need scripts that run only in specific environments (e.g., seeding mock user data in DEV or QA, but skipping it in PREPROD / PROD).
Liquibase handles this natively via Contexts.
5. How Context Filtering Works
Instead of wrapping logic in SQL IF/ELSE conditions, you annotate changesets with a context attribute:
<!-- This changeset runs ONLY when the active context matches QA or DEV -->
<changeSet id="seed-test-users" author="aman" context="QA, DEV">
<sqlFile path="release-1.0/INSERT_TEST_DATA.sql" />
</changeSet>
When running Liquibase, pass the current environment context via property file or CLI arguments:
liquibase --defaults-file=environments/QA.properties --contexts=QA update
6. End-to-End Spring Boot Integration
Let's look at how to run Liquibase natively inside a Spring Boot application using pure SQL files and custom rollbacks.
Step 1: Add Dependencies (pom.xml)
<dependency>
<groupId>org.liquibase</groupId>
<artifactId>liquibase-core</artifactId>
</dependency>
<dependency>
<groupId>com.microsoft.sqlserver</groupId>
<artifactId>mssql-jdbc</artifactId>
<scope>runtime</scope>
</dependency>
Step 2: Configure application.yml
Point Spring Boot to your primary changelog location and define the active Liquibase context matching your Spring profile:
spring:
profiles:
active: dev
datasource:
url: jdbc:sqlserver://localhost:1433;databaseName=app_db;trustServerCertificate=true
username: sa
password: YourPassword
liquibase:
change-log: classpath:db/changelog/db.changelog-master.xml
enabled: true
contexts: ${spring.profiles.active}
Step 3: Project File Structure in Spring Boot
Place your changelog and SQL scripts inside src/main/resources:
src/main/resources/
└── db/
├── changelog/
│ ├── db.changelog-master.xml
│ └── releases/
│ ├── 001-create-orders-table.sql
│ └── 002-seed-qa-data.sql
Step 4: Write the Master Changelog (db.changelog-master.xml)
<?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">
<!-- Changeset 1: Create Orders Table -->
<changeSet id="001-create-orders-table" author="amanjot">
<comment>Creating the primary orders table for release 1.0</comment>
<sqlFile path="releases/001-create-orders-table.sql" relativeToChangelogFile="true"/>
<rollback>
<sqlFile path="releases/001-create-orders-table-rollback.sql" relativeToChangelogFile="true"/>
</rollback>
</changeSet>
<!-- Tagging release version in DATABASECHANGELOG table -->
<changeSet id="tag-v1.0.0" author="amanjot">
<tagDatabase tag="v1.0.0"/>
</changeSet>
<!-- Changeset 2: Seed QA/Dev Test Data -->
<changeSet id="002-seed-qa-data" author="amanjot" context="dev, qa">
<comment>Seeding mock order records for Dev and QA environments only</comment>
<sqlFile path="releases/002-seed-qa-data.sql" relativeToChangelogFile="true"/>
<rollback>
<sqlFile path="releases/002-seed-qa-data-rollback.sql" relativeToChangelogFile="true"/>
</rollback>
</changeSet>
</databaseChangeLog>
Step 5: Format SQL Scripts with Rollbacks
You can write pure SQL files while taking advantage of Liquibase tracking using Formatted SQL annotations (--changeset and --rollback):
File: releases/001-create-orders-table.sql
--liquibase formatted sql
--changeset aman:001-create-orders
CREATE TABLE orders (
id BIGINT IDENTITY(1,1) PRIMARY KEY,
order_number VARCHAR(50) NOT NULL UNIQUE,
total_amount DECIMAL(10, 2) NOT NULL,
created_at DATETIME2 DEFAULT GETDATE()
);
--rollback DROP TABLE orders;
File: releases/002-seed-qa-data.sql (Environment Specific)
--liquibase formatted sql
--changeset aman:002-seed-qa-data context:dev,qa
INSERT INTO orders (order_number, total_amount)
VALUES ('ORD-TEST-001', 99.99), ('ORD-TEST-002', 149.50);
--rollback DELETE FROM orders WHERE order_number IN ('ORD-TEST-001', 'ORD-TEST-002');
Step 6: Triggering Rollbacks via CLI / Maven
When Boot starts, it applies all pending scripts automatically. If you need to revert a release in a pipeline using Maven or the Liquibase CLI:
-- Roll back the last 1 applied changeset
mvn liquibase:rollback -Dliquibase.rollbackCount=1
-- Roll back to a specific version tag
mvn liquibase:tag -Dliquibase.tag=v1.0.0
-- ... if issues arise later:
mvn liquibase:rollback -Dliquibase.rollbackTag=v1.0.0
Conclusion
Managing database migrations shouldn't feel like walking a tightrope without a net. By decoupling environment configs, and leveraging context tags, your database schema stays as version-controlled and reliable as your application codebase.
Top comments (1)
Some comments may only be visible to logged-in visitors. Sign in to view all comments.