When two transitive dependencies require different versions of the same module, what does Gradle do by default?
answer
- one version per module per configuration
- highest requested version wins
- module identity = group:name
- dependencies / dependencyInsight to diagnose
- version ordering, not string compare
basics
~10 sBy default Gradle picks the highest requested version of the module and uses that single version everywhere on the classpath. This is 'highest-version-wins' conflict resolution.
solid answer
~40 sWhen the dependency graph requests several versions of the same module (same group:name), Gradle's default conflict resolution selects the **highest** version among all the requested versions and aligns the whole graph to it — one version per module per configuration. 'Highest' is determined by Gradle's version-ordering rules, not plain string compare (it understands `1.10` > `1.9`, qualifiers like `-rc`, `-SNAPSHOT`, etc.). This is intentional: a single classpath cannot have two copies of the same module, and the newer version is assumed backwards-compatible. The losing version is evicted. You can see the winner with `./gradlew dependencies` (it annotates the chosen version) or, for a specific module, `./gradlew dependencyInsight --dependency <name>`, which shows exactly why a version was selected.
code
bash · 6 lines# See the whole resolved graph for one configuration
./gradlew dependencies --configuration runtimeClasspath
# Focus on one module and learn WHY a version was chosen
./gradlew dependencyInsight --configuration runtimeClasspath --dependency guava
# -> guava:30.0-jre -> 31.0-jre (by conflict resolution: between versions ...)go deeper
Know the headline: same module requested at different versions -> Gradle keeps the highest, one version per classpath.
Explain module identity = group:name, per-configuration resolution, and that 'highest' uses version ordering not string compare; name the diagnostic tasks.
Contrast with Maven's nearest-wins, explain that it's a graph-level metadata decision before download, and discuss when the heuristic is wrong (major bumps).
Frame highest-wins as the org-wide default policy, its risk surface for breaking changes, and when to escalate to failOnVersionConflict + governed overrides.
## The problem A Java/Kotlin runtime classpath can hold only **one** version of a given class. But real dependency graphs are messy: library A pulls in `guava:31.0`, library B pulls in `guava:30.0`. Both can't be present, so Gradle must pick one. The rule that decides which is the **conflict resolution strategy**. ## Gradle's default: highest-version-wins Gradle's default strategy is `latest` / highest-version-wins. Among every version *requested* for a module (`group:name`), Gradle selects the **highest** and uses it for the entire build's view of that module within a configuration. The other requests are *evicted*; their declaring dependencies now resolve against the winner. Key points: - 'Module' identity is `group:name` (the version is excluded from identity). Two artifacts with the same group:name are the *same module* and therefore conflict. - 'Highest' uses Gradle's **version ordering**, not lexicographic string compare. It splits on `.`, `-`, `_`, `+`, compares numeric parts numerically (`1.10 > 1.9`), and applies special handling to qualifiers (`dev < rc < release`, `-SNAPSHOT` is special). - Resolution happens **per configuration** (e.g. `compileClasspath`, `runtimeClasspath`) and per project — each resolves independently. - This is a *graph* decision, made before artifacts are downloaded, using metadata (POM/Gradle Module Metadata). ## Diagnosing what happened Two built-in tasks: - `./gradlew dependencies` — prints the full resolved tree per configuration. A line like `guava:30.0 -> 31.0` means 30.0 was requested but resolved to 31.0 (it lost the conflict). `(*)` marks a subtree already shown. - `./gradlew dependencyInsight --configuration runtimeClasspath --dependency guava` — focuses on **one** module and prints the *reasons* for the selected version (e.g. 'by conflict resolution: between versions 30.0 and 31.0'). ## Why default to highest? Under semantic versioning, a higher minor/patch is meant to be backwards-compatible, so picking the newest is the safest automatic choice. It is a *heuristic*, not a guarantee — a major-version bump can break callers, which is why you sometimes need to override (forcing, constraints, etc. — separate topics). ```kotlin dependencies { implementation("com.google.guava:guava:30.0-jre") // direct implementation("some.lib:foo:1.0") // foo pulls guava:31.0-jre transitively } // runtimeClasspath resolves guava -> 31.0-jre (highest wins) ``` Run `./gradlew dependencyInsight --dependency guava` and you'll see the selected `31.0-jre` with reason 'by conflict resolution'.
- Is 'highest' decided by string comparison?No. Gradle uses version-ordering rules: it splits on separators and compares numeric parts numerically, so 1.10 > 1.9, and handles qualifiers (rc, SNAPSHOT) specially rather than alphabetically.
- Does the conflict get resolved once for the whole build?No — resolution is per configuration (and per project). compileClasspath and runtimeClasspath each resolve independently, though they usually land on the same version.
Like a meeting room that fits only one projector: if two people bring projectors, you use the newer model and the old one sits out — everyone shares the one that's plugged in.
saying these in an interview costs you the question
- Saying Gradle keeps both versions on the classpath — it cannot; one is evicted.
- Claiming highest-wins compares versions as plain strings.
- Confusing Gradle's default (highest) with Maven's 'nearest definition wins'.