Liquibase context and precondition gating: smallest design for environment-specific changesets
Use one master changelog with contexts for environment gating and preconditions for state assumptions. Keep DATABASECHANGELOG as the source of truth and plan for locks, partial DDL, and precondition masking.
10 Jul 2025, 23:46 UTC

The problem is changelog sprawl, not missing features
Teams often fork changelogs per environment or add manual if-statements in deployment scripts. The useful takeaway is that one master changelog with contexts for environment gating and preconditions for state assumptions is usually enough. It keeps a single source of truth while allowing predictable degradation when the database is already in the expected state.
Requirements for environment-gated migrations
A design must satisfy three constraints without adding process overhead.
- One changelog per product, not per environment. Developers review one history.
- Changesets run only when the runtime matches the intended target. A seed data load should not run in production.
- Assumptions about existing state must be explicit. If a table may already exist from a hotfix, the migration should not fail mid-deploy.
Contexts and labels address the first two. Preconditions address the third. Both are declarative and evaluated at runtime by Liquibase before a changeset is applied.
Smallest suitable design
Keep a master changelog that includes versioned files. Use includeAll for automatic discovery, noting that ordering is alphabetical by path. Renaming files can reorder execution and break dependencies.
<databaseChangeLog>
<includeAll path="db/changelog"/>
</databaseChangeLog>
Tag changesets with context only for true environment differences, not as a feature flag.
changeset author:id
context: dev,seed-data
<createTable tableName="test_data">...</createTable>
Use preconditions where state genuinely varies, and choose onFail/onError deliberately.
changeset author:id
precondition onFail="MARK_RAN" onError="HALT"
<tableExists tableName="orders"/>
<addColumn tableName="orders">...</addColumn>
Precondition types such as tableExists or sqlCheck let a changeset declare assumptions. MARK_RAN records the changeset as run without executing it. WARN logs but continues. HALT stops the update. CONTINUE proceeds regardless. The smallest design uses preconditions sparingly, only where drift is expected, and reserves contexts for environment selection.
Trust and data boundaries
Liquibase executes whatever SQL the changelog author wrote with the privileges of the migration user. The changelog is trusted code. Features like contexts and preconditions do not sandbox execution.
The operational source of truth is DATABASECHANGELOG and DATABASECHANGELOGLOCK. DATABASECHANGELOG records id, author, filename, checksum, context, and EXECTYPE. EXECTYPE values include EXECUTED for a normal run and MARK_RAN for a precondition-skipped changeset. DATABASECHANGELOGLOCK is a single-row lock that prevents concurrent updates.
Checksum validation is intentional integrity protection. Editing an already applied changeset triggers a checksum mismatch on the next update. The correct remediation is a new changeset or an explicit clearCheckSums, not editing history.
Operational checks
Run liquibase updateSQL against a scratch database before applying to production. This requires read access to the changelog and read access to the target database schema, but does not modify state. Inspect the generated SQL for the chosen context filter.
liquibase --url=${DB_URL} --username=${DB_USER} --password=${DB_PASS} --contexts=prod updateSQL
After a migration, query DATABASECHANGELOG to confirm expected changesets, contexts, and EXECTYPE.
SELECT ID, AUTHOR, FILENAME, CONTEXTS, EXECTYPE FROM DATABASECHANGELOG WHERE FILENAME LIKE '%yourfile%';
Simulate a killed migration in a non-production environment to verify lock recovery. Stop the process mid-run, then confirm a row exists in DATABASECHANGELOGLOCK and that liquibase releaseLocks clears it. This requires migration user privileges to update the lock table.
Deliberately modify an already applied changeset in a test environment and rerun update to observe checksum validation behavior. This confirms integrity checks are active.
Failure modes to plan for
Lock left behind after a killed migration. The lock prevents further updates until released. ReleaseLocks is the recovery path.
Partially applied non-transactional DDL. On databases like MySQL, some DDL statements auto-commit. If the process fails after a partial batch, the database may be left inconsistent. Preconditions do not make DDL transactional.
Precondition masking. MARK_RAN and CONTINUE can hide real drift. A changeset that silently never executed appears as completed in DATABASECHANGELOG. Prefer HALT during early rollout and relax only with monitoring.
Rollback gaps. Liquibase can generate rollback for some changes, but refactorings like dropColumn are not automatically derivable. Assume forward-fix unless explicit rollback changesets are written and tested.
When this design changes
The smallest design holds until requirements shift.
- Drift detection across many databases. When you need automated comparison of schema state across dozens of tenants, add diff tooling and a reporting step outside Liquibase.
- Automated rollback guarantees. If deployments require guaranteed revert windows, structured rollback blocks and tested rollback changesets become necessary.
- Multi-tenant schema-per-tenant rollout. When rollout must be staged per tenant with different timing, consider external orchestration that invokes Liquibase per tenant with tenant-specific contexts, or a commercial offering with targeted rollback.
Behavior details for precondition types and attribute names vary between XML, YAML, JSON and SQL changelog formats and across Liquibase versions. Verify syntax against the format you use.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.