In some situations, you might want to be in total control of the dependency graph. In particular, you may want to make sure that:

  • the versions declared in a build script actually correspond to the ones being resolved

  • or make sure that dependency resolution is reproducible over time

Gradle provides ways to perform this by configuring the resolution strategy.

Failing on version conflict

There’s a version conflict whenever Gradle finds the same module in two different versions in a dependency graph. By default, Gradle performs optimistic upgrades, meaning that if version 1.1 and 1.3 are found in the graph, we resolve to the highest version, 1.3. However, it is easy to miss that some dependencies are upgraded because of a transitive dependency. In the example above, if 1.1 was a version used in your build script and 1.3 a version brought transitively, you could use 1.3 without actually noticing.

To make sure that you are aware of such upgrades, Gradle provides a mode that can be activated in the resolution strategy of a configuration. Imagine the following dependencies declaration:

Example 1. Direct dependency version not matching a transitive version
build.gradle
dependencies {
    implementation 'org.apache.commons:commons-lang3:3.0'
    // the following dependency brings lang3 3.8.1 transitively
    implementation 'com.opencsv:opencsv:4.6'
}
build.gradle.kts
dependencies {
    implementation("org.apache.commons:commons-lang3:3.0")
    // the following dependency brings lang3 3.8.1 transitively
    implementation("com.opencsv:opencsv:4.6")
}

Then by default Gradle would upgrade commons-lang3, but it is possible to fail the build:

Example 2. Fail on version conflict
build.gradle
configurations.all {
    resolutionStrategy {
        failOnVersionConflict()
    }
}
build.gradle.kts
configurations.all {
    resolutionStrategy {
        failOnVersionConflict()
    }
}

Making sure resolution is reproducible

There are cases where dependency resolution can be unstable over time. That is to say that if you build at date D, building at date D+x may give a different resolution result.

This is possible in the following cases:

  • dynamic dependency versions are used (version ranges, latest.release, 1.+, …​)

  • or changing versions are used (SNAPSHOTs, fixed version with changing contents, …​)

The recommended way to deal with dynamic versions is to use dependency locking. However, it is possible to prevent the use of dynamic versions altogether, which is an alternate strategy:

Example 3. Failing on dynamic versions
build.gradle
configurations.all {
    resolutionStrategy {
        failOnDynamicVersions()
    }
}
build.gradle.kts
configurations.all {
    resolutionStrategy {
        failOnDynamicVersions()
    }
}

Likewise, it’s possible to prevent the use of changing versions by activating this flag:

Example 4. Failing on changing versions
build.gradle
configurations.all {
    resolutionStrategy {
        failOnChangingVersions()
    }
}
build.gradle.kts
configurations.all {
    resolutionStrategy {
        failOnChangingVersions()
    }
}

It’s a good practice to fail on changing versions at release time.

Eventually, it’s possible to combine both failing on dynamic versions and changing versions using a single call:

Example 5. Failing on non-reproducible resolution
build.gradle
configurations.all {
    resolutionStrategy {
        failOnNonReproducibleResolution()
    }
}
build.gradle.kts
configurations.all {
    resolutionStrategy {
        failOnNonReproducibleResolution()
    }
}