This guide explains how to prevent accidental dependency versions due to Gradle’s eager dependency resolution.

Why Prevent Accidental Dependency Upgrades?

When managing dependencies in Gradle, you may encounter situations where transitive dependencies cause unexpected upgrades to newer versions. This can lead to unintended behavior or compatibility issues. Gradle, by default, performs optimistic upgrades, meaning it resolves to the highest available version when multiple versions of a dependency are found in the dependency graph.

For example, if both 3.1 and 3.2 versions of commons-lang3 are present, Gradle will select 3.2, even if your build script explicitly declares 3.1. If your build uses a feature available in version 3.1 but not 3.2, or if you haven’t updated your build to be compatible with version 3.2, you may not want this eager upgrade from Gradle’s dependency resolution process.

build.gradle.kts
dependencies {
    implementation("org.apache.commons:commons-lang3:3.2")

    constraints {
        implementation("org.apache.commons:commons-lang3:3.1") {
            because("Version 1.3 introduces breaking changes not yet handled")
        }
    }
}
build.gradle
dependencies {
    implementation("org.apache.commons:commons-lang3:3.2")

    constraints {
        implementation("org.apache.commons:commons-lang3:3.1") {
            because("Version 1.3 introduces breaking changes not yet handled")
        }
    }
}

Running ./gradlew dependencies --configuration runtimeClasspath showcases the results:

dependencies.out
> Task :dependencies

------------------------------------------------------------
Root project 'how_to_prevent_accidental_upgrades'
------------------------------------------------------------

runtimeClasspath - Compile classpath for source set 'main'.
+--- org.apache.commons:commons-lang3:3.2
\--- org.apache.commons:commons-lang3:3.1 -> 3.2 (c)

Option 1: Enforce Strict Dependency Resolution

Gradle provides an option to fail the build when a version conflict occurs, ensuring that no unintended upgrades happen.

To enable this, configure your build to fail on version conflicts:

build.gradle.kts
configurations.all {
    resolutionStrategy.failOnVersionConflict()
}
build.gradle
configurations.all {
    resolutionStrategy.failOnVersionConflict()
}

When this setting is enabled, Gradle will stop execution and report an error if multiple versions of a dependency conflict, instead of automatically upgrading to the highest version.

Running ./gradlew dependencies --configuration runtimeClasspath showcases the failure:

dependencies-fail.out
> Task :dependencies FAILED

------------------------------------------------------------
Root project 'how_to_prevent_accidental_upgrades'
------------------------------------------------------------

runtimeClasspath - Runtime classpath of source set 'main'.

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':dependencies'.
> Could not resolve all dependencies for configuration ':runtimeClasspath'.
   > Conflict found for the following module:
       - org.apache.commons:commons-lang3 between versions 3.2 and 3.1

Option 2: Using Dependency Constraints

If you have multiple dependencies that use a shared library, you can enforce a consistent version across all modules using dependency constraints:

build.gradle.kts
dependencies {
    implementation("org.apache.commons:commons-lang3")
    constraints {
        implementation("org.apache.commons:commons-lang3") {
            version {
                strictly("3.1")
            }
        }
    }
}
build.gradle
dependencies {
    implementation("org.apache.commons:commons-lang3")
    constraints {
        implementation("org.apache.commons:commons-lang3") {
            version {
                strictly("3.1")
            }
        }
    }
}

With strictly("3.1"), Gradle ensures that no other version can override the specified dependency.

Running ./gradlew dependencies --configuration runtimeClasspath showcases the results:

dependencies-const.out
> Task :dependencies

------------------------------------------------------------
Root project 'how_to_prevent_accidental_upgrades'
------------------------------------------------------------

runtimeClasspath - Runtime classpath of source set 'main'.
+--- org.apache.commons:commons-lang3 -> 3.1
\--- org.apache.commons:commons-lang3:{strictly 3.1} -> 3.1 (c)

Option 3: Locking Dependencies to Specific Versions

For a more robust solution, you can use dependency locking to ensure Gradle resolves the same dependency versions consistently across builds.

To enable dependency locking:

build.gradle.kts
configurations.all {
    resolutionStrategy.activateDependencyLocking()
}
build.gradle
configurations.all {
    resolutionStrategy.activateDependencyLocking()
}

Then, generate a lock file:

./gradlew dependencies --write-locks

This creates a gradle.lockfile that stores the exact dependency versions, preventing future upgrades unless explicitly updated.

Summary

Gradle’s optimistic dependency resolution may inadvertently upgrade dependencies, causing compatibility issues.

To prevent unexpected upgrades, you can:

  • Enforce strict resolution (failOnVersionConflict)

  • Explicitly constrain dependencies using the strictly keyword

  • Use dependency locking (activateDependencyLocking) to maintain consistent versions across builds.