Back to CI/CD Pipeline
Q22Question 22 of 56

What Liquibase is and how it manages database schema version control: changeSets, changelogs, databasechangelog

Key Concepts
  • Liquibase is an open-source database schema change-management tool that brings version control to the database the way git brings it to code.
  • A changeSet is the atomic unit of change, uniquely identified by id, author, and filename, and once applied it is immutable.
  • A changelog is the ordered master file listing changeSets; running it against any database applies only what is missing.
  • DATABASECHANGELOG is a tracking table that records every applied changeSet with its checksum, making runs idempotent.
  • DATABASECHANGELOGLOCK prevents two instances from migrating simultaneously.
  • Liquibase supports expand-and-contract for zero-downtime deployments by ordering changeSets across releases.

Definition

Liquibase is git for your database schema: ordered changeSets in a changelog, and a DATABASECHANGELOG table that remembers what has already been applied so each environment converges to the same structure safely.
  • 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.

DIAGRAM
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.

XML
<!-- 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">>
SQL
-- 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.

XML
<!-- 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.

SQL
-- What DATABASECHANGELOG looks like
SELECT id, author, filename, md5sum, dateexecuted, orderexecuted
  FROM DATABASECHANGELOG
 ORDER BY orderexecuted;
TEXT
| 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.

XML
<!-- 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">>
YAML
# application.yml
spring:
  liquibase:
    change-log: classpath:db/changelog/db.changelog-master.xml
    enabled: true
BASH
# 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.

XML
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">>
BASH
# 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.

XML
<!-- 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.

TEXT
# 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...
XML
<!-- 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.

XML
<!-- 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

DIAGRAM
  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.

---

Share
Use ← → arrow keys to navigate