skip to content

Why can the same module resolve to different versions in different configurations, and how does that relate to default conflict resolution?

level: seniorimportance: should knowfreq 38%

answer

  1. resolution is per configuration
  2. compile vs runtime graphs differ
  3. runtimeOnly can pull a higher transitive
  4. dependencyInsight needs the configuration
  5. compile/runtime skew -> NoSuchMethodError

basics

~10 s

Each resolvable configuration (compileClasspath, runtimeClasspath, etc.) is resolved independently, so highest-version-wins runs separately per configuration. Different requested-version sets can therefore yield different winners.

solid answer

~40 s

Gradle resolves dependencies **per configuration**, not once for the whole project. `compileClasspath` and `runtimeClasspath` (and test variants) each build their own dependency graph from their own declared dependencies and inherited buckets, then run highest-version-wins **within that graph**. Because the set of requested versions can differ — e.g. a `runtimeOnly` dependency pulls a newer transitive that compile doesn't see — the same module can legitimately be selected at version X for compile and Y for runtime. This is by design but a frequent source of confusion: `dependencyInsight` therefore requires you to name the configuration. It also means `failOnVersionConflict()` must be considered per configuration (hence `configurations.all { ... }`). When you need one consistent version everywhere, you drive it from a platform/constraint rather than relying on per-configuration resolution to coincide.

code

bash · 4 lines
bash
# Same module, two configurations, possibly two answers:
./gradlew dependencyInsight --configuration compileClasspath --dependency lib:core
./gradlew dependencyInsight --configuration runtimeClasspath --dependency lib:core
# core may resolve to 1.0 for compile and 2.0 for runtime.

go deeper

for a junior

Know that compile and runtime classpaths are resolved separately.

for a middle

Explain why runtimeOnly/compileOnly cause differing graphs and why dependencyInsight needs --configuration.

for a senior

Connect per-configuration resolution to compile/runtime skew bugs and to why failOnVersionConflict uses configurations.all.

for a principal

Advocate platform/BOM-driven versions to eliminate skew org-wide rather than relying on coincidental winners.

## Configurations are independent resolution scopes A Gradle **configuration** is a named bucket of dependencies. Some are *resolvable* (you can ask them for a classpath): `compileClasspath`, `runtimeClasspath`, `testCompileClasspath`, `testRuntimeClasspath`, etc. Each resolvable configuration: 1. Gathers its declared + inherited dependencies (via `extendsFrom`), 2. Builds a dependency graph, 3. Applies conflict resolution (highest-version-wins by default) **within that graph**. Crucially, step 3 happens **independently per configuration**. There is no global 'project resolved this module to version Z'. ## How divergence arises The configurations don't see identical dependency sets: - `implementation` flows to both compile and runtime; `compileOnly` only to compile; `runtimeOnly` only to runtime. - A `runtimeOnly` library might drag in `lib:2.0` transitively while compile only ever sees `lib:1.0`. Compile resolves 1.0, runtime resolves 2.0 — both correctly highest-wins **for their own graph**. - Annotation-processor or test configurations can diverge even more. ```kotlin dependencies { implementation("lib:core:1.0") // both compile & runtime see core:1.0 runtimeOnly("some:driver:5.0") // only runtime sees driver -> may pull core:2.0 } // compileClasspath: core -> 1.0 // runtimeClasspath: core -> 2.0 (highest wins within runtime graph) ``` ## Practical consequences - **Diagnosis must name the configuration:** `dependencyInsight --configuration runtimeClasspath ...`. Inspecting the wrong one explains nothing. - **`failOnVersionConflict()` is per configuration:** that's why teams apply `configurations.all { resolutionStrategy.failOnVersionConflict() }` to cover every resolvable bucket. - **Compile/runtime skew bugs:** code compiles against an older API but runs against a newer (or vice versa), causing `NoSuchMethodError` at runtime. This is exactly the kind of skew that per-configuration resolution can hide. - **Fix with cross-cutting version control:** a platform/BOM or dependency constraints apply to all configurations consistently, eliminating skew instead of hoping the per-configuration winners coincide. ## Mental model Think of each configuration as a separate mini-project that happens to share declarations. Highest-wins is local to each. The 'project version of guava' is a fiction; the honest question is always 'the guava version *in this configuration*'.

  • What runtime bug can compile/runtime version skew cause?
    Code compiled against an older API can hit a NoSuchMethodError/NoSuchFieldError at runtime when the runtime classpath resolved a different (higher or lower) version of the module.
  • How do you guarantee one consistent version across all configurations?
    Drive the version from a platform/BOM or dependency constraints, which apply across configurations, rather than relying on each configuration's highest-wins to coincide.

saying these in an interview costs you the question

  • Claiming a project resolves each module to a single global version.
  • Diagnosing with dependencyInsight on the wrong configuration and trusting the result.
  • Assuming compileClasspath and runtimeClasspath always agree.

context