Part 24 of 24

Best Practices Reference: The Complete Liquibase Checklist

Twenty-three articles of Liquibase knowledge, distilled into one reference. Bookmark this page. Use the checklists before every production deployment. Come back when something breaks.

This article doesn’t repeat the “why” — that’s in the earlier articles. This is the what: concrete rules, with the article number if you need the full explanation.


The 7 Laws (Non-Negotiable)

These apply without exception. Break them and you will eventually have a bad day in production.

  1. A deployed changeset is immutable. Never edit a changeset that has been applied to any environment. Create a new changeset instead. (Articles 2, 23)

  2. One logical change per changeset. Each changeset should do exactly one thing. Atomic changesets enable precise rollback and clean audit trails. (Article 2)

  3. Every production changeset needs a rollback. Write the rollback block the moment you write the changes block. (Articles 9, 16)

  4. Tag before every production deployment. Run liquibase tag vX.Y.Z before liquibase update. Always. (Articles 9, 16)

  5. Preview before applying. Run liquibase updateSQL before liquibase update in production. Read the SQL. (Articles 4, 16)

  6. Validate rollback in CI. Run liquibase future-rollback-sql in your pipeline. Rollbacks that fail in CI save you from rollbacks that fail at 2am. (Articles 16, 19)

  7. Never use ddl-auto: create/update with Liquibase. Set spring.jpa.hibernate.ddl-auto=none. One tool manages the schema. (Articles 6, 22)


Changeset Writing Checklist

Use this when writing every new changeset.

Identity

  • ID is unique across the entire project (format: YYYY-MM-DD-NNN-description)
  • Author is your GitHub username (not “admin”, “user”, or “test”)
  • The filename includes a date or version prefix to sort correctly

Content

  • One logical change per changeset
  • Changeset is idempotent where possible (using preconditions)
  • No sensitive data (passwords, PII) in data migration changesets
  • Column types are MySQL-compatible (BIGINT not INTEGER, DATETIME(6) not TIMESTAMP for precision)
  • Last changeset in each release file is a tagDatabase changeset marking the release version (see Article 2)

Rollback

  • Rollback block is present
  • Rollback block is correct (will actually undo the change)
  • For raw SQL changesets: rollback SQL is explicitly written
  • For destructive changes (dropTable, dropColumn): rollback recreates the object with the same definition

For Production-Ready Changesets

  • Preconditions added where appropriate (adoption, optional objects)
  • Context or label set if this is environment-specific
  • runOnChange: true set only for views/procedures/triggers, not structural changes
  • failOnError: true confirmed (the default) — you want hard failures

Changelog Organization Checklist

Structure

  • Master changelog (db.changelog-master.yaml) only includes other files — no changesets directly in it
  • Files organized by version or date directory (v1.0/, v1.1/, or 2026-05/)
  • File naming uses numeric prefixes for reliable ordering (001-, 002-)
  • Seed/test data is in a separate directory from schema migrations

Master Changelog Pattern

# db.changelog-master.yaml
databaseChangeLog:
  - include:
      file: db/changelog/v1.0/1.0-baseline.yaml
  - include:
      file: db/changelog/v1.1/1.1-001-add-user-preferences.yaml
  - include:
      file: db/changelog/v1.1/1.1-002-add-product-tags.yaml

includeAll Safety Rules

  • Numeric prefixes on all files in includeAll directories
  • filter attribute set to prevent accidental inclusion of non-migration files
  • Test/seed files are in a separate directory, not mixed with migrations
  • You’ve verified the alphabetical sort order matches logical dependency order

MySQL-Specific Checklist

Column Types

  • Use BIGINT for primary keys, not INT (you’ll thank yourself at 2 billion rows)
  • Use DATETIME(6) for timestamps with microsecond precision, not TIMESTAMP (which has a 2038 limit)
  • Use DECIMAL(10,2) for monetary values, never FLOAT or DOUBLE
  • Use VARCHAR(255) for most text fields — match your JPA entity annotation
  • Use TEXT only when values will genuinely exceed 65KB

Character Set

  • Table character set is utf8mb4, not utf8 (which is a MySQL misnomer for 3-byte UTF-8)
  • Collation is utf8mb4_unicode_ci or utf8mb4_0900_ai_ci (MySQL 8 default)
  • Use modifySql in changesets if the database default charset isn’t utf8mb4

Large Table Migrations

  • Checked whether ALTER TABLE will lock the table (ALGORITHM=INSTANT vs COPY)
  • For tables > 1M rows, planned use of Percona pt-online-schema-change or MySQL’s ALGORITHM=INPLACE
  • Adding columns at the end of the table (not in the middle) when possible
  • Index additions run during low-traffic periods

Stored Objects

  • Stored procedures use splitStatements: false and endDelimiter: $$
  • Views, procedures, and functions use runOnChange: true (they’re replaceable)
  • DROP + CREATE pattern used for MySQL (no CREATE OR REPLACE PROCEDURE in MySQL)

Spring Boot Integration Checklist

Configuration (Every Environment)

spring:
  jpa:
    hibernate:
      ddl-auto: none          # REQUIRED — always
  liquibase:
    enabled: true
    change-log: classpath:db/changelog/db.changelog-master.yaml
  • ddl-auto: none set in every profile (dev, test, staging, prod)
  • spring.liquibase.enabled=true (default, but verify no profile overrides it to false)
  • Changelog path is correct and the file exists on the classpath
  • JDBC URL includes serverTimezone=UTC for MySQL 8

Per-Environment Configuration

# application-dev.yaml
spring:
  liquibase:
    contexts: dev
    drop-first: false         # NEVER true in staging/prod

# application-test.yaml
spring:
  liquibase:
    contexts: test
    # or use Testcontainers — see Article 20
  • drop-first: false verified in non-local environments
  • Context matches the Spring profile for environment-specific changesets
  • Test configuration uses either H2 (simple) or Testcontainers (accurate)

Actuator

  • /actuator/liquibase endpoint enabled in staging for deployment verification
  • Actuator endpoints are not exposed publicly in production

CI/CD Pipeline Checklist

On Every Pull Request

# These must pass before merge
- liquibase validate           # syntax + checksums
- liquibase future-rollback-sql  # rollback is possible
- liquibase updateSQL          # preview SQL (store as artifact)
  • liquibase validate runs as the first database step
  • liquibase future-rollback-sql runs and produces non-empty output for changesets with changes
  • liquibase updateSQL output is stored as a CI artifact for DBA review

On Deploy to Staging

- liquibase tag staging-$(date +%Y%m%d-%H%M%S)
- liquibase update
- liquibase status             # verify 0 pending changesets after update
  • Tag before update
  • Status check after update confirms 0 pending changesets
  • Smoke test runs against the updated schema

On Deploy to Production

- liquibase tag v$VERSION-pre-deploy
- liquibase update
- liquibase status
  • Human approval gate before production deploy
  • Tag includes version number (not just a timestamp)
  • Rollback procedure documented in the deployment ticket/PR
  • On-call notified of deployment window

Production Deployment Runbook

T-5 min: Confirm staging passed all pipeline checks

T-2 min:

liquibase status --verbose    # confirm expected changesets pending
liquibase updateSQL           # final SQL review

T-0:

liquibase tag v1.5.0-pre-deploy
liquibase update

T+2 min:

liquibase status              # should show: "All changesets have been run"
liquibase history             # verify changesets applied with correct timestamps

If anything is wrong:

liquibase rollback v1.5.0-pre-deploy

T+15 min: Confirm application is healthy, no errors in logs


Rollback Decision Tree

Migration failed during deployment?
├── Did any changesets complete before the failure?
│   ├── YES → Roll back to pre-deploy tag:
│   │         liquibase rollback v1.5.0-pre-deploy
│   └── NO  → No schema changes were made; fix the changelog and redeploy
│
Migration completed but application is broken?
├── Is the schema change reversible?
│   ├── YES → liquibase rollback v1.5.0-pre-deploy
│   └── NO  → Apply a forward-fix changeset (drop-column is gone; add it back differently)
│
Lock is stuck?
└── liquibase release-locks, then retry

Team Workflow Checklist

For Every Developer

  • Know the changeset ID naming convention used by your team
  • Never run DDL directly against any shared environment (dev, staging, prod)
  • Create changesets in a feature branch; add to master changelog before merging
  • Review the DATABASECHANGELOG after your changeset runs to confirm it applied correctly

For Code Reviews

  • Changeset ID is unique and follows the naming convention
  • One logical change per changeset
  • Rollback block is present and makes sense
  • No sensitive data in data changesets
  • MySQL-compatible column types used
  • Preconditions added where appropriate

For the Team Lead / DBA

  • Run liquibase diff weekly to catch schema drift
  • Review liquibase history before and after each deployment
  • Rotate database credentials for the Liquibase user separately from the application user
  • The Liquibase DB user should have CREATE, ALTER, DROP, INSERT, UPDATE, DELETE — not more

Security Checklist

  • Liquibase credentials stored in Vault / AWS Secrets Manager, not in application.yaml
  • Separate database user for Liquibase vs application (Liquibase needs DDL rights; app should not)
  • liquibase.properties is in .gitignore; no credentials committed to source control
  • Changelog files do not contain passwords, API keys, or PII (even in comments)
  • In Kubernetes: Liquibase credentials are Kubernetes Secrets, not ConfigMaps

Performance Checklist

  • Indexes are added in separate changesets from table creation (easier to rollback)
  • Large index additions include a comment about estimated duration
  • Foreign key constraints verified to have supporting indexes on the child table
  • loadData changesets for bulk inserts use batchSize attribute
  • ON UPDATE CURRENT_TIMESTAMP columns used intentionally (not applied to every datetime column)

The Pre-Production Gate

Run these three commands before every production deployment. If any fails, do not deploy.

# 1. Validate changeset syntax, checksums, and structure
liquibase validate \
  --changelog-file=db/changelog/db.changelog-master.yaml \
  --url=jdbc:mysql://staging:3306/ecommerce \
  --username=liquibase_user \
  --password=$LIQUIBASE_PASS

# 2. Verify rollback is possible for every pending changeset
liquibase future-rollback-sql \
  --changelog-file=db/changelog/db.changelog-master.yaml \
  --url=jdbc:mysql://staging:3306/ecommerce \
  --username=liquibase_user \
  --password=$LIQUIBASE_PASS \
  --output-file=rollback-preview.sql

# 3. Preview the exact SQL that will run
liquibase update-sql \
  --changelog-file=db/changelog/db.changelog-master.yaml \
  --url=jdbc:mysql://staging:3306/ecommerce \
  --username=liquibase_user \
  --password=$LIQUIBASE_PASS \
  --output-file=deploy-preview.sql

Quick Reference: Common Commands

GoalCommand
Apply pending changesetsliquibase update
Preview pending SQLliquibase updateSQL
Show pending changesetsliquibase status --verbose
Show all applied changesetsliquibase history
Validate syntax and checksumsliquibase validate
Tag current stateliquibase tag vX.Y.Z
Rollback to tagliquibase rollback vX.Y.Z
Rollback N changesetsliquibase rollback-count N
Rollback to dateliquibase rollback-to-date "2026-05-20 14:30:00"
Preview rollback SQLliquibase future-rollback-sql
Test rollback round-tripliquibase update-testing-rollback
Release stuck lockliquibase release-locks
Clear all checksumsliquibase clearCheckSums
Compare two databasesliquibase diff
Take a DB snapshotliquibase snapshot --snapshot-format=json
Generate changelog from DBliquibase generateChangelog
Sync without applyingliquibase changelogSync
Preview sync SQLliquibase changelogSyncSQL
Rollback one changeset (Secure)liquibase rollback-one-changeset --changeset-id=... --force
Rollback one deployment (Secure)liquibase rollback-one-update --deploy-id=... --force

Liquibase Secure: Features Worth Knowing

Everything in this series uses Liquibase Community. These Liquibase Secure features are worth knowing about as your use matures:

Policy Checks — define rules that are validated against changelogs before every deployment. Examples: require every changeset to have a rollback block, ban DROP TABLE without a precondition, enforce naming conventions. Runs in CI via the CLI or a flow file; not available as a Spring Boot startup check.

Flow Files — YAML-defined pipeline orchestration. Replaces shell scripts that chain Liquibase commands. A flow file runs validate → tag → updateSQL → update → status as a single declarative unit, with conditional steps (e.g., “only run update if there are pending changesets”).

# liquibase.flowfile.yaml (Secure)
stages:
  validate:
    actions:
      - type: liquibase
        command: validate
  deploy:
    actions:
      - type: liquibase
        command: tag
        cmdArgs: {tag: "${DEPLOY_TAG}"}
      - type: liquibase
        command: update

rollback-one-changeset and rollback-one-update — roll back a single identified changeset or a complete deployment batch without reverting everything back to a tag. Covered in Article 9.

For Community users: Policy Checks can be partially replicated with futureRollbackSQL in CI (catches missing rollback blocks), and Flow Files can be replaced with shell scripts. The Community approach is well-covered in this series and handles production requirements for most teams.


The Series at a Glance

You’ve covered the full Liquibase stack across 24 articles:

PartArticlesWhat You Learned
Part 1: Foundations1–6Core concepts, changelog formats, first migration, commands, Spring Boot
Part 2: Everyday Patterns7–12Changelog org, migration patterns, rollback, contexts, preconditions, property substitution
Part 3: Production Patterns13–18Stored objects, MySQL specifics, reverse engineering, production rollback, zero-downtime, team workflow
Part 4: CI/CD & Operations19–24GitHub Actions, testing, Kubernetes, existing DB adoption, troubleshooting, this reference

The most important thing isn’t knowing every flag — it’s having the discipline to follow the seven laws every time. The teams that have bad Liquibase experiences are usually the teams that modified a deployed changeset once, skipped a rollback block twice, and ran direct SQL “just this once.”

The teams that use Liquibase well run boring deployments. Boring is the goal.