Definition
- Liquibase – an open-source database schema change-management tool that makes schema evolution repeatable, ordered, and identical across every environment.
- changeSet – the atomic unit of change (create table, add column, create index), uniquely identified by id, author, and filename.
- changelog – the ordered master file that lists changeSets or includes other changelog files, often one per release.
- DATABASECHANGELOG – a tracking table Liquibase creates inside the database, recording every applied changeSet with its id, author, checksum, and execution date.
- DATABASECHANGELOGLOCK – a companion table that holds a lock so two instances do not migrate simultaneously.
- Checksum – a hash of each changeSet that Liquibase verifies on every run; editing an applied changeSet causes a mismatch and Liquibase refuses to run.
Imagine National Bank of India keeps a master ledger of every structural change ever made to its record-keeping system. Each entry is numbered and signed, and the bank keeps a checklist of which changes each branch has already carried out. When a new branch opens, staff run down the list in order, skipping anything already done, so every branch ends up with exactly the same structure. Liquibase is that master ledger and checklist.
Understanding the Concepts
What problem does Liquibase solve?
Databases drift. Without a tool, one developer runs a CREATE TABLE by hand, another forgets to apply it to staging, and production ends up subtly different. Liquibase makes the schema a versioned, reproducible artifact that travels with the code.
Without Liquibase: Dev DB: accounts table has iban column Staging DB: accounts table missing iban column ← DRIFT! Prod DB: accounts table missing iban column ← DRIFT! With Liquibase: Same changelog runs everywhere → identical schema
What is a changeSet?
A changeSet is the atomic unit. It has three identifying parts: an id, an author, and the file it lives in. The golden rule is that once committed and applied, a changeSet is immutable.
<!-- db/changelog/changes/001-create-accounts.xml --syn-kw">>
syn-kw"><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-4.27.xsd"syn-kw">>
syn-kw"><changeSet id="001" author="rajesh"syn-kw">>
syn-kw"><createTable tableName="accounts"syn-kw">>
syn-kw"><column name="id" type="BIGINT" autoIncrement="true"syn-kw">>
syn-kw"><constraints primaryKey="true" nullable="false"/syn-kw">>
syn-kw"></columnsyn-kw">>
syn-kw"><column name="account_number" type="VARCHAR(20)"syn-kw">>
syn-kw"><constraints nullable="false" unique="true"/syn-kw">>
syn-kw"></columnsyn-kw">>
syn-kw"><column name="balance" type="DECIMAL(19,2)" defaultValue="0.00"syn-kw">>
syn-kw"><constraints nullable="false"/syn-kw">>
syn-kw"></columnsyn-kw">>
syn-kw"><column name="created_at" type="TIMESTAMP" defaultValueComputed="CURRENT_TIMESTAMP"/syn-kw">>
syn-kw"></createTablesyn-kw">>
syn-kw"></changeSetsyn-kw">>
syn-kw"></databaseChangeLogsyn-kw">>
-- Equivalent SQL changeSet format
-- db/changelog/changes/001-create-accounts.sql
-- changeset rajesh:001
CREATE TABLE accounts (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
account_number VARCHAR(20) NOT NULL UNIQUE,
balance DECIMAL(19,2) NOT NULL DEFAULT 0.00,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- rollback DROP TABLE accounts;
What is a changelog?
A changelog is the ordered master file that lists changeSets or includes other files. A common structure is a root changelog that includes one file per release.
<!-- db/changelog/db.changelog-master.xml --syn-kw">>
syn-kw"><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-4.27.xsd"syn-kw">>
syn-kw"><include file="changes/001-create-accounts.xml"
relativeToChangelogFile="true"/syn-kw">>
syn-kw"><include file="changes/002-add-iban-column.xml"
relativeToChangelogFile="true"/syn-kw">>
syn-kw"><include file="changes/003-create-transfers.xml"
relativeToChangelogFile="true"/syn-kw">>
syn-kw"></databaseChangeLogsyn-kw">>
How does DATABASECHANGELOG track state?
The first time Liquibase runs, it creates the DATABASECHANGELOG table automatically. For every changeSet it applies, it inserts a row. On subsequent runs, it reads this table and executes only changeSets not already recorded.
-- What DATABASECHANGELOG looks like
SELECT id, author, filename, md5sum, dateexecuted, orderexecuted
FROM DATABASECHANGELOG
ORDER BY orderexecuted;
| id | author | filename | md5sum | dateexecuted | order |
|-----|--------|-------------------------------|----------|---------------------|-------|
| 001 | rajesh | changes/001-create-accounts | 8a3f... | 2025-01-10 09:00:00 | 1 |
| 002 | rajesh | changes/002-add-iban-column | 7b2e... | 2025-01-12 14:30:00 | 2 |
| 003 | priya | changes/003-create-transfers | 9c1d... | 2025-01-15 11:00:00 | 3 |
How do you integrate Liquibase with Spring Boot?
Liquibase runs automatically at application startup when the Spring Boot starter is on the classpath.
<!-- pom.xml --syn-kw">>
syn-kw"><dependencysyn-kw">>
syn-kw"><groupIdsyn-kw">>org.liquibasesyn-kw"></groupIdsyn-kw">>
syn-kw"><artifactIdsyn-kw">>liquibase-coresyn-kw"></artifactIdsyn-kw">>
syn-kw"></dependencysyn-kw">>
# application.yml
spring:
liquibase:
change-log: classpath:db/changelog/db.changelog-master.xml
enabled: true
# On startup, Liquibase applies any new changeSets
2025-01-15 11:00:00 INFO [liquibase] Successfully acquired change log lock
2025-01-15 11:00:01 INFO [liquibase] Reading from DATABASECHANGELOG
2025-01-15 11:00:01 INFO [liquibase] ChangeSet changes/003-create-transfers.xml::003::priya ran successfully
2025-01-15 11:00:01 INFO [liquibase] Successfully released change log lock
How does Liquibase support rollback?
A changeSet can declare how to undo itself, or Liquibase can auto-generate rollback for simple changes.
syn-kw"><changeSet id="002" author="rajesh"syn-kw">>
syn-kw"><addColumn tableName="accounts"syn-kw">>
syn-kw"><column name="iban" type="VARCHAR(34)"/syn-kw">>
syn-kw"></addColumnsyn-kw">>
syn-kw"><rollbacksyn-kw">>
syn-kw"><dropColumn tableName="accounts" columnName="iban"/syn-kw">>
syn-kw"></rollbacksyn-kw">>
syn-kw"></changeSetsyn-kw">>
# Roll back the last 2 changeSets
liquibase rollback-count 2
How does Liquibase enable zero-downtime deployments?
For rolling updates where old and new code run simultaneously, schema changes must be backward compatible. Liquibase executes each step as a separate, ordered changeSet.
<!-- Release 2.5: EXPAND -- add new column --syn-kw">>
syn-kw"><changeSet id="004" author="rajesh"syn-kw">>
syn-kw"><addColumn tableName="accounts"syn-kw">>
syn-kw"><column name="iban" type="VARCHAR(34)"/syn-kw">>
syn-kw"></addColumnsyn-kw">>
syn-kw"></changeSetsyn-kw">>
<!-- Release 2.6: MIGRATE -- backfill data --syn-kw">>
syn-kw"><changeSet id="005" author="rajesh"syn-kw">>
syn-kw"><sqlsyn-kw">>UPDATE accounts SET iban = CONCAT('IN', sort_code, account_number)
WHERE iban IS NULLsyn-kw"></sqlsyn-kw">>
syn-kw"></changeSetsyn-kw">>
<!-- Release 2.7: CONTRACT -- remove old column --syn-kw">>
syn-kw"><changeSet id="006" author="rajesh"syn-kw">>
syn-kw"><preConditions onFail="MARK_RAN"syn-kw">>
syn-kw"><columnExists tableName="accounts" columnName="sort_code"/syn-kw">>
syn-kw"></preConditionssyn-kw">>
syn-kw"><dropColumn tableName="accounts" columnName="sort_code"/syn-kw">>
syn-kw"></changeSetsyn-kw">>
What are common mistakes?
The most dangerous mistake is editing an already-applied changeSet. Liquibase stores a checksum, and a mismatch causes a startup failure.
# ERROR when an applied changeSet is modified:
Validation Failed:
1 changeSets check sum changed
changes/001-create-accounts.xml::001::rajesh
was: 8a3f... but is now: 5d7b...
<!-- WRONG: editing an applied changeSet --syn-kw">>
syn-kw"><changeSet id="001" author="rajesh"syn-kw">>
syn-kw"><createTable tableName="accounts"syn-kw">>
<!-- Changed column type from BIGINT to UUID -- BREAKS! --syn-kw">>
syn-kw"><column name="id" type="UUID"/syn-kw">>
syn-kw"></createTablesyn-kw">>
syn-kw"></changeSetsyn-kw">>
<!-- CORRECT: add a new changeSet instead --syn-kw">>
syn-kw"><changeSet id="007" author="rajesh"syn-kw">>
syn-kw"><modifyDataType tableName="accounts" columnName="id" newDataType="UUID"/syn-kw">>
syn-kw"></changeSetsyn-kw">>
What should a senior engineer know?
A senior engineer uses preconditions so changeSets are defensive, contexts so test seed data runs only in development, and keeps destructive changes separated from the release that stops using the old structure.
<!-- Precondition: only add column if it does not exist --syn-kw">>
syn-kw"><changeSet id="008" author="rajesh"syn-kw">>
syn-kw"><preConditions onFail="MARK_RAN"syn-kw">>
syn-kw"><notsyn-kw">>
syn-kw"><columnExists tableName="accounts" columnName="iban"/syn-kw">>
syn-kw"></notsyn-kw">>
syn-kw"></preConditionssyn-kw">>
syn-kw"><addColumn tableName="accounts"syn-kw">>
syn-kw"><column name="iban" type="VARCHAR(34)"/syn-kw">>
syn-kw"></addColumnsyn-kw">>
syn-kw"></changeSetsyn-kw">>
<!-- Context: only run in dev --syn-kw">>
syn-kw"><changeSet id="009" author="rajesh" context="dev"syn-kw">>
syn-kw"><insert tableName="accounts"syn-kw">>
syn-kw"><column name="account_number" value="TEST001"/syn-kw">>
syn-kw"><column name="balance" value="100000.00"/syn-kw">>
syn-kw"></insertsyn-kw">>
syn-kw"></changeSetsyn-kw">>
Visual Overview
Developer workflow:
───────────────────
1. Add changeSet to changelog
2. Commit with code to git
3. CI/CD pipeline deploys
4. Liquibase runs at startup
│
▼
┌──────────────────────────────────────┐
│ Read DATABASECHANGELOGLOCK │
│ Acquire lock │
│ │ │
│ ▼ │
│ Read DATABASECHANGELOG │
│ Compare with changelog file │
│ │ │
│ ├── changeSet 001: already applied (skip)
│ ├── changeSet 002: already applied (skip)
│ └── changeSet 003: NEW → execute │
│ │ │
│ ▼ │
│ Record in DATABASECHANGELOG │
│ Release lock │
└──────────────────────────────────────┘
Key Takeaway
Liquibase brings version control to the database the way git brings it to code. Every schema change is a changeSet with a unique identity, and once applied it is immutable: Liquibase enforces this with checksums. ChangeSets live in a changelog that is the ordered timeline of the schema, and DATABASECHANGELOG tracks what each database has already applied, making every run idempotent.
The practical payoff is that development, staging, and production schemas stay identical because the same changelog runs everywhere. Liquibase also supports rollback, preconditions for defensive migrations, and contexts for environment-specific data.
The connection to zero-downtime deployments is direct. Expand-and-contract is executed as separate ordered changeSets across releases: one release adds the new column, a later release removes the old one. At National Bank of India, that discipline is what made the schema a trustworthy, auditable artifact rather than a source of production surprises.
Remember one sentence: Liquibase is the ordered, checksummed, idempotent changelog that makes every database converge to the same schema safely.
---