Managing Environment-Specific Changes with Liquibase Contexts
Learn how to use Liquibase contexts to manage environment-specific database migrations using a single changelog without code duplication.
17 Sept 2024, 02:17 UTC

Problem and Takeaway
Database migration teams often maintain separate changelogs for dev, test, and prod, leading to divergence, duplicated effort, and the risk of applying the wrong changeset to the wrong environment. Liquibase contexts provide a declarative way to tag changesets so that a single master changelog can drive environment-selective execution. The practical takeaway: by assigning a context attribute to each changeset and passing the --contexts flag, you can guarantee that only the intended changesets run in each environment without copying files or relying on script branching.
Requirements
- A Liquibase project (version 3.6 or later; 4.x adds expression-based context features).
- A master changelog file (typically db.changelog.xml or referenced via liquibase.yml).).
- Database access credentials with sufficient privileges to create tables, alter schemas, and read/write the
DATABASECHANGELOGtable. - Awareness that context names are case-sensitive and that the
DATABASECHANGELOGprovides idempotence across repeated updates.
Smallest Suitable Design
The minimal working model is a single master changelog where every changeset declares its intended contexts. Here is a concrete example:
<changeSet id="create_users_table" author="team" context="dev,prod">
<createTable tableName="users">
<Column name="id" type="BIGINT"/gt;
<Column name="username" type="VARCHAR(255)"/gt;
</createTable>
</changeSet>In this changeset, the context attribute lists the environments in which the changeset may run. If a changeset has no context attribute, Liquibase applies it to every environment. This design eliminates changelog duplication. Liquibase stores a record of every applied changeset in the DATABASECHANGELOG table, including its context. On subsequent updates, Liquibase cross-references this table to skip changesets already executed for the given contexts. This mechanism provides idempotence: running the same update command twice does not re-apply changes. The trust boundary rests on the database user’s ability to modify DATABASECHANGELOG; only users with write access (typically the migration owner) should hold this privilege.
Operational Checks
- From the project root (where liquibase.properties or liquibase.yml resides), run:
liquibase update --contexts=devObserve which changesets execute. Only those with
context="dev"or containing "dev" (e.g., "dev,prod") should run. Required permission: the database user must have INSERT/UPDATE onDATABASECHANGELOGand DDL rights on the target schema. Risk: running with an incorrect context may apply changes to the wrong environment. - Check the
DATABASECHANGELOGtable:SELECT * FROM DATABASECHANGELOG WHERE context LIKE '%dev%';You should see only the dev-tagged changesets marked as executed. Expected check: the
EXECUTEDcolumn is 1 for those rows. - Switch contexts and confirm isolation:
liquibase update --contexts=prodThen query
DATABASECHANGELOGfor prod-tagged entries. Changesets tagged only "dev" should remain pending (i.e., not appear in the result). This verifies that context filtering works. - Run
liquibase status --contexts=devto list pending changesets for that environment without applying them. The output shows id, author, and context, helping you audit what will run. Expected check: no output for already-applied dev changesets; pending ones list their context.
Failure Modes and Conditions That Would Change the Design
Misspelled or mismatched context names: Liquibase treats contexts as case-sensitive strings. A changeset with context="Prod" will not match a command using --contexts=prod, leaving it pending or, if already executed in a different case, potentially causing drift. Always double-check context spelling.
No context on a changeset: If a changeset lacks the context attribute, it runs in every environment. This defeats isolation and may inadvertently propagate dev-only data (e.g., test fixtures) to prod. Review the changelog for any context-less changesets when adding new environments.
Overlapping contexts with high cardinality: Maintaining dozens of overlapping context strings (e.g., "dev", "development", "dev-eu", "dev-us") reduces readability and increases the chance of accidental omission. Group related contexts or use Liquibase 4.x expression features to simplify logic.
Dynamic, runtime-determined changesets: When an environment requires changesets that are not known at design time—for example, tenant-specific schema alterations or feature-flag-driven migrations—the static context model may be insufficient. In such cases, consider using preconditions, custom listeners, or splitting the changelog per service/team.
When to reconsider the design: If your workflow requires changesets to be selected based on parameters that cannot be expressed as a fixed set of context names (e.g., per-tenant IDs, feature toggles stored in a config table, or conditional logic dependent on application state), the static context approach becomes a maintenance burden. Evaluate whether a combination of preconditions, dynamic properties, or per-service changogs fits the requirement.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.