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.
A deployed changeset is immutable. Never edit a changeset that has been applied to any environment. Create a new changeset instead. (Articles 2, 23)
One logical change per changeset. Each changeset should do exactly one thing. Atomic changesets enable precise rollback and clean audit trails. (Article 2)
Every production changeset needs a rollback. Write the
rollbackblock the moment you write thechangesblock. (Articles 9, 16)Tag before every production deployment. Run
liquibase tag vX.Y.Zbeforeliquibase update. Always. (Articles 9, 16)Preview before applying. Run
liquibase updateSQLbeforeliquibase updatein production. Read the SQL. (Articles 4, 16)Validate rollback in CI. Run
liquibase future-rollback-sqlin your pipeline. Rollbacks that fail in CI save you from rollbacks that fail at 2am. (Articles 16, 19)Never use
ddl-auto: create/updatewith Liquibase. Setspring.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 (
BIGINTnotINTEGER,DATETIME(6)notTIMESTAMPfor precision) - Last changeset in each release file is a
tagDatabasechangeset 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: trueset only for views/procedures/triggers, not structural changes -
failOnError: trueconfirmed (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/, or2026-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
includeAlldirectories -
filterattribute 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
BIGINTfor primary keys, notINT(you’ll thank yourself at 2 billion rows) - Use
DATETIME(6)for timestamps with microsecond precision, notTIMESTAMP(which has a 2038 limit) - Use
DECIMAL(10,2)for monetary values, neverFLOATorDOUBLE - Use
VARCHAR(255)for most text fields — match your JPA entity annotation - Use
TEXTonly when values will genuinely exceed 65KB
Character Set
- Table character set is
utf8mb4, notutf8(which is a MySQL misnomer for 3-byte UTF-8) - Collation is
utf8mb4_unicode_ciorutf8mb4_0900_ai_ci(MySQL 8 default) - Use
modifySqlin changesets if the database default charset isn’t utf8mb4
Large Table Migrations
- Checked whether
ALTER TABLEwill lock the table (ALGORITHM=INSTANTvsCOPY) - For tables > 1M rows, planned use of Percona
pt-online-schema-changeor MySQL’sALGORITHM=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: falseandendDelimiter: $$ - Views, procedures, and functions use
runOnChange: true(they’re replaceable) - DROP + CREATE pattern used for MySQL (no
CREATE OR REPLACE PROCEDUREin 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: noneset 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=UTCfor 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: falseverified in non-local environments - Context matches the Spring profile for environment-specific changesets
- Test configuration uses either H2 (simple) or Testcontainers (accurate)
Actuator
-
/actuator/liquibaseendpoint 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 validateruns as the first database step -
liquibase future-rollback-sqlruns and produces non-empty output for changesets with changes -
liquibase updateSQLoutput 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
DATABASECHANGELOGafter 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 diffweekly to catch schema drift - Review
liquibase historybefore 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.propertiesis 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
-
loadDatachangesets for bulk inserts usebatchSizeattribute -
ON UPDATE CURRENT_TIMESTAMPcolumns 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
| Goal | Command |
|---|---|
| Apply pending changesets | liquibase update |
| Preview pending SQL | liquibase updateSQL |
| Show pending changesets | liquibase status --verbose |
| Show all applied changesets | liquibase history |
| Validate syntax and checksums | liquibase validate |
| Tag current state | liquibase tag vX.Y.Z |
| Rollback to tag | liquibase rollback vX.Y.Z |
| Rollback N changesets | liquibase rollback-count N |
| Rollback to date | liquibase rollback-to-date "2026-05-20 14:30:00" |
| Preview rollback SQL | liquibase future-rollback-sql |
| Test rollback round-trip | liquibase update-testing-rollback |
| Release stuck lock | liquibase release-locks |
| Clear all checksums | liquibase clearCheckSums |
| Compare two databases | liquibase diff |
| Take a DB snapshot | liquibase snapshot --snapshot-format=json |
| Generate changelog from DB | liquibase generateChangelog |
| Sync without applying | liquibase changelogSync |
| Preview sync SQL | liquibase 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:
| Part | Articles | What You Learned |
|---|---|---|
| Part 1: Foundations | 1–6 | Core concepts, changelog formats, first migration, commands, Spring Boot |
| Part 2: Everyday Patterns | 7–12 | Changelog org, migration patterns, rollback, contexts, preconditions, property substitution |
| Part 3: Production Patterns | 13–18 | Stored objects, MySQL specifics, reverse engineering, production rollback, zero-downtime, team workflow |
| Part 4: CI/CD & Operations | 19–24 | GitHub 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.