Designing Environment‑Specific Migrations with Liquibase Contexts
Use a single master changelog with context attributes to run environment‑specific migrations in Liquibase. This guide covers the minimal design, trust boundaries, CI checks, failure modes, and when to split changelogs.
15 Aug 2025, 08:10 UTC

Problem and Takeaway
Teams often need a single source‑controlled changelog that behaves differently in development, test, and production. Liquibase contexts solve this by tagging change sets so they execute only when a matching context is active. The key takeaway: a minimal, version‑controlled master changelog with context attributes, combined with CI‑enforced context selection, gives you environment isolation without duplicating migration files.
Requirements
- One authoritative changelog stored in Git.
- Change sets must be selectively applied per environment (dev, test, prod).
- Rollback scripts must respect the same environment boundaries.
- Developers need fast feedback locally; CI must guarantee the same selection logic.
Smallest Suitable Design
Use a single master.xml (or YAML/JSON) that includes every change set. Each <changeSet> carries a context attribute listing the environments where it should run. At runtime the Liquibase CLI or Maven/Gradle plugin receives --contexts=dev (or test, prod) and evaluates the attribute list.
<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.20.xsd">
<changeSet id="1" author="alice" context="dev,test">
<createTable tableName="audit_log">
<column name="id" type="bigint" autoIncrement="true">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="event" type="varchar(255)"/>
</createTable>
</changeSet>
<changeSet id="2" author="bob" context="prod">
<sql>CREATE EXTENSION IF NOT EXISTS pgcrypto;</sql>
</changeSet>
<changeSet id="3" author="carol" context="dev,test,prod">
<addColumn tableName="users">
<column name="last_login" type="timestamp" nullable="true"/>
</addColumn>
</changeSet>
</databaseChangeLog>
Only three change sets, each tagged with the environments that need them. No duplicate changelogs, no conditional logic inside the SQL.
Trust and Data Boundaries
- Version control is the single source of truth; every change set is reviewed before merge.
- Context gating prevents a developer’s
dev-only data‑seed script from ever reaching production because the production pipeline never passes--contexts=dev. - Rollback parity: each change set that needs an explicit rollback should carry the same context list, ensuring
liquibase rollbackCount 1 --contexts=prodonly undoes production‑tagged changes.
Operational Checks
- Local validation – run
liquibase validateafter each commit to catch malformed context strings. - CI gate – a pipeline step executes
liquibase status --contexts=prodagainst a staging copy of the production schema. The step fails if any change set markedprodshowsPending. - Dry‑run – before a real deployment, run
liquibase updateSQL --contexts=prodto inspect the generated SQL without touching the database. - Post‑deploy verification – execute
liquibase status --contexts=prodagain; allprodchange sets must beRan.
Failure Modes
| Symptom | Root Cause | Mitigation |
|---|---|---|
| Change set silently skipped in prod | Typo in context name (e.g., prodd) | Enforce a naming convention (lower‑case, no spaces) and run liquibase validate in CI. |
| Dev‑only data appears in staging | Staging pipeline accidentally includes dev context | Separate pipeline variables per environment; audit the --contexts value in each job. |
| Rollback removes production schema objects | Rollback change set missing context="prod" | Tag every rollback change set with the same contexts as its forward counterpart. |
| Large changelog becomes unreadable | Hundreds of change sets each listing many contexts | Group related changes into logical modules and use include files; keep context lists short (max 2‑3 per change set). |
When the Design Must Change
- Multiple independent schemas owned by different teams – split into separate master changelogs per schema, each with its own context namespace (e.g.,
billing-prod,shipping-prod). - Regulatory data‑residency rules – if certain migrations must never run in a region, add a region context (
eu,us) and gate pipelines accordingly. - Feature‑flag driven releases – contexts can be combined with Liquibase labels for finer granularity, but avoid over‑loading contexts; consider a separate feature‑flag table instead.
Concrete Diagnostic Decision
Assume a developer reports that the audit_log table is missing in the test environment. The quick check:
# Run from the project root where liquibase.properties defines the test DB URL
liquibase status --contexts=test
If the output shows the change set id=1 as Pending, the context list on that change set likely omits test. Fix by editing the changelog, commit, and let CI re‑run the validation gate.
Verification Checklist (Run After Any Changelog Change)
liquibase validate– exits 0 if XML/YAML syntax and context attributes are well‑formed.liquibase update --contexts=devon a local dev DB – confirm onlydev‑tagged sets apply.liquibase status --contexts=prodon a staging DB – allprodsets must beRan.liquibase rollbackCount 1 --contexts=prodon staging – verify the rollback SQL matches expectations (dry‑run withrollbackSQLfirst).
Limitations
- Context evaluation is purely runtime; there is no compile‑time guarantee that a context exists in any pipeline.
- Case sensitivity:
Dev≠dev. Adopt a project‑wide convention and enforce it with a linting script. - Rollback does not automatically filter by context; you must tag rollback change sets explicitly.
- Very large changelogs (>2000 change sets) can slow
statusandvalidate; modular includes mitigate this.
Practical Way to Confirm the Design Works
Create a throwaway Docker container with a clean PostgreSQL instance, point liquibase.properties at it, then run the full verification checklist for each context. The container can be torn down after the test, giving a repeatable, isolated proof that contexts behave as intended.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.