skip to content

Two dependencies in your graph provide the same capability. What happens, and how do you resolve the conflict?

level: seniorimportance: should knowfreq 35%

answer

  1. capabilitiesResolution.withCapability { select(...) }
  2. selectHighestVersion()
  3. component metadata rule adds the shared capability
  4. exclude or dependencySubstitution
  5. dependencyInsight to diagnose

basics

~10 s

Gradle fails resolution with a capability conflict. You resolve it with resolutionStrategy.capabilitiesResolution.withCapability("g:n") { select(...) }, or by excluding/replacing one module so only one variant provides the capability.

solid answer

~40 s

When two variants in the resolved graph expose the **same capability** (and they're not just two versions of the same module that Gradle auto-resolves by version), Gradle stops with a **capability conflict** error — it refuses to put both on the classpath because they'd provide the same thing twice. You resolve it deterministically: ```kotlin configurations.all { resolutionStrategy.capabilitiesResolution.withCapability("com.google.guava:guava") { select("com.google.guava:guava:0") } } ``` `select(...)` picks the winning candidate (you can also `selectHighestVersion()` or pick by reason). Alternatives: declare a **component metadata rule** that maps both onto the same capability so the conflict is *detected*, then exclude the loser, or substitute one module for the other with `resolutionStrategy.dependencySubstitution`. The key idea: capability conflicts are a feature — they surface 'same thing, different coordinates' clashes early instead of letting duplicate classes blow up at runtime.

code

kotlin · 9 lines
kotlin
configurations.all {
    resolutionStrategy.capabilitiesResolution {
        withCapability("com.google.guava:guava") {
            // pick a specific candidate
            select("com.google.guava:guava:0")
            because("guava supersedes google-collections")
        }
    }
}

go deeper

for a junior

Know that two modules providing the same capability cause a build failure you must resolve.

for a middle

Name capabilitiesResolution.select and the exclude alternative.

for a senior

Walk through detecting via metadata rules, resolving with select/selectHighestVersion/substitution, and diagnosing with dependencyInsight.

for a principal

Establish org-wide capability rules (e.g. a convention plugin that de-dupes known clashing pairs) so every build resolves them consistently.

## Why a conflict happens Gradle resolves a dependency graph into a set of **variants**. A rule of resolution: **no two selected variants may provide the same capability**. There are two flavours: 1. **Same module, different versions** — both provide the implicit GAV capability `g:n`. Gradle resolves this *automatically* by version conflict resolution (highest wins by default). No error. 2. **Different modules, same explicit capability** — e.g. `guava` and `google-collections` both declared to provide `com.google.guava:guava`. Gradle **cannot** auto-pick (different versions, different artifacts) so it raises a **capability conflict** and fails the build. ## Detecting the conflict For third-party modules that don't declare shared capabilities, you add a **component metadata rule**: ```kotlin dependencies { components.withModule("com.google.collections:google-collections") { allVariants { withCapabilities { addCapability("com.google.guava", "guava", "1.0") } } } } ``` Now both provide `com.google.guava:guava` → Gradle detects the clash. ## Resolving the conflict ### 1. capabilitiesResolution.select ```kotlin configurations.all { resolutionStrategy.capabilitiesResolution.withCapability("com.google.guava:guava") { select("com.google.guava:guava:0") // pick guava, drop google-collections } } ``` You can also call `selectHighestVersion()` inside the block, or inspect `getCandidates()` and choose programmatically with a `because("...")` reason. ### 2. Exclude the loser ```kotlin dependencies { implementation("some:lib:1.0") { exclude(group = "com.google.collections") } } ``` ### 3. Dependency substitution ```kotlin configurations.all { resolutionStrategy.dependencySubstitution { substitute(module("com.google.collections:google-collections")) .using(module("com.google.guava:guava:31.1-jre")) .because("google-collections is superseded by guava") } } ``` ## Feature-variant conflicts The same machinery resolves conflicts between **feature variants**: if two modules expose the same feature capability (e.g. via shading), you `select` the intended one. This is exactly how Gradle prevents two implementations of one capability — like two test-fixtures or two 'logging-backend' features — landing together. ## Diagnosing `./gradlew dependencyInsight --configuration runtimeClasspath --dependency guava` and the build scan show which capability clashed and why a candidate was chosen.

  • Why doesn't 'two versions of the same module' raise a capability conflict error?
    Both expose the same implicit GAV capability, but Gradle has a built-in rule (highest version wins) to auto-resolve same-module version conflicts, so it picks one silently instead of failing.
  • What's the difference between select() and selectHighestVersion() in capabilitiesResolution?
    select(candidate) picks a specific named candidate; selectHighestVersion() asks Gradle to pick the candidate with the highest version among those providing the capability.
  • How would you make Gradle even detect that guava and google-collections clash?
    Add a component metadata rule (components.withModule { withCapabilities { addCapability(...) } }) so both advertise the same capability; only then is there a conflict to resolve.

saying these in an interview costs you the question

  • Claiming Gradle silently keeps both modules — it fails on an explicit capability conflict.
  • Confusing version conflict resolution (auto) with capability conflict resolution (requires a rule).

context