Resolution, conflicts and BOMs

Gradle · Advanced · 12 min

Diamond conflicts and vulnerable transitives are everyday problems; Gradle gives precise tools to fix them.

When the dependency graph is resolved, each module ends up with exactly one version per configuration. Gradle picks the highest requested version by default; this differs from Maven's nearest-wins rule. ./gradlew dependencyInsight --dependency <name> --configuration runtimeClasspath shows every path and why a version won.

Tools to shape the result:

  • Platforms / BOMs: implementation(platform("org.springframework.boot:spring-boot-dependencies:3.4.4")) imports a Maven BOM so all its modules get aligned versions. enforcedPlatform overrides even higher requests.
  • Rich versions: implementation("com.foo:bar") { version { strictly("[1.2, 2.0[") ; prefer("1.4") } }.
  • Constraints adjust transitive versions without adding a direct dependency (e.g. to fix a CVE).
  • Exclusions: implementation("x:y:1.0") { exclude(group = "commons-logging") } drops an unwanted transitive.
  • Substitution in configurations.all { resolutionStrategy.dependencySubstitution { ... } } replaces a module with another or with a project.
  • resolutionStrategy.failOnVersionConflict() makes conflicts explicit.
  • Capabilities detect mutually exclusive libraries (e.g. two logging bindings).

Dependency locking (dependencyLocking { lockAllConfigurations() } and ./gradlew dependencies --write-locks) records resolved versions in gradle.lockfile, so builds stay reproducible even with ranges. Dependency verification checks checksums and signatures of downloaded artifacts.

Separate configurations (compileClasspath, runtimeClasspath, testRuntimeClasspath) may resolve to different versions, which is expected.

Example

plugins { java }
repositories { mavenCentral() }

dependencies {
    implementation(platform("org.springframework.boot:spring-boot-dependencies:3.4.4"))
    implementation("org.springframework.boot:spring-boot-starter-web")   // version from the BOM

    implementation("org.apache.httpcomponents.client5:httpclient5:5.4.3") {
        exclude(group = "commons-logging", module = "commons-logging")
    }
    implementation("com.fasterxml.jackson.core:jackson-databind") {
        version { strictly("[2.18, 2.19[") }
    }
    constraints {
        implementation("org.yaml:snakeyaml:2.4") { because("fix CVE in transitive version") }
    }
}

configurations.all {
    resolutionStrategy {
        failOnVersionConflict()
        cacheChangingModulesFor(0, "seconds")
    }
}

dependencyLocking { lockAllConfigurations() }   // $ ./gradlew dependencies --write-locks
// $ ./gradlew dependencyInsight --dependency snakeyaml --configuration runtimeClasspath

Remember

  • Gradle resolves conflicts to the highest version unless told otherwise
  • platform(BOM) aligns versions across a family of modules
  • Constraints, strict versions and exclusions adjust transitives
  • Locking and verification make resolution reproducible and safe

Check yourself

  1. Two transitive paths request library X at 1.2 and 1.5. Which does Gradle pick by default?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.gradle.org/current/userguide/dependency_constraints.html