Preventing Migration Failures with Liquibase Preconditions
Stop fighting 'already exists' errors in your database migrations. Learn how to use Liquibase Preconditions to create resilient, environment-aware schema updates.
14 Mar 2026, 03:44 UTC

The 'Already Exists' Migration Headache
Database migrations often fail during merge conflicts or environment synchronization. A common scenario occurs when two developers create different feature branches that both add a column to the same table. When these branches merge, the first migration succeeds, but the second triggers a "column already exists" error, halting the entire deployment pipeline.
The solution is to move from imperative migrations (do this) to conditional migrations (do this only if the state is X). In Liquibase, this is achieved through Preconditions.
What are Preconditions?
Preconditions are logic checks executed immediately before a ChangeSet runs. Instead of assuming the database is in a specific state, Liquibase queries the database metadata to verify if the change is actually necessary or safe to perform.
The behavior of a ChangeSet when a precondition fails is governed by the onFail attribute, which supports three primary strategies:
- HALT: The default behavior. Stops the entire migration process and marks the deployment as failed. Use this for critical dependencies.
- MARK_RAN: The ChangeSet is skipped, but Liquibase records it as "executed" in the
DATABASECHANGELOGtable. This prevents the migration from attempting to run again in future deployments. - WARN: The ChangeSet is skipped, and a warning is logged, but it is not marked as ran. It will be evaluated again during the next update.
Managing Environment Drift
Preconditions are particularly useful when dealing with "drift"—where a production database has been manually patched or differs slightly from the development environment. By combining preconditions with Contexts (logical tags like prod or dev), you can ensure that a cleanup script runs in development but is safely ignored in production without creating separate files for every environment.
Worked Example: Conditional Column Addition
Consider a scenario where you need to add a user_preference column to a users table, but you cannot guarantee that a previous manual hotfix hasn't already added it.
Below is a configuration using XML format (compatible with Liquibase 4.x+). This should be placed in your changelog file.
<changeSet id="add-user-pref-column" author="tech-editor">
<preConditions>
<not>
<columnExists tableName="users" columnName="user_preference"/>
</not>
<!-- If the column already exists, skip this set and mark it as complete --
<onFail value="MARK_RAN"/>
</preConditions>
<addColumn tableName="users">
<column name="user_preference" type="varchar(255)"/>
</addColumn>
</changeSet>
Execution and Verification
To apply this change, run the following command from your terminal or CI/CD runner (assuming you have the Liquibase CLI installed and a liquibase.properties file configured):
liquibase update
Verification steps:
- Run the command once: The column is created, and the
DATABASECHANGELOGtable records the success. - Manually add the column to a fresh database, then run the command: The precondition fails,
onFail="MARK_RAN"triggers, and the migration completes without an error. - Check the
DATABASECHANGELOGtable: The ChangeSet should be listed as executed even though theaddColumnlogic was skipped.
Trade-offs and Limitations
While preconditions add resilience, they introduce runtime overhead. Every precondition requires a metadata query to the database. In massive changelogs with hundreds of preconditions, this can marginally increase the time it takes for the update command to initialize.
The most significant risk is the use of MARK_RAN. If a precondition is written incorrectly (e.g., checking for the wrong table name), Liquibase will mark the migration as complete without actually applying the change. This creates a "silent failure" where your schema is out of date, but your logs indicate a successful deployment.
Actionable Summary
To harden your database pipeline, stop writing "blind" migrations. Use columnExists or tableExists preconditions with onFail="MARK_RAN" for any additive changes that might overlap across feature branches. Always verify your precondition logic in a staging environment before deploying to production to avoid silent skips.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.