How do you resolve a capability conflict programmatically in Gradle, picking one variant over another?
answer
- resolutionStrategy.capabilitiesResolution.withCapability
- candidates list + select(candidate)
- selectHighestVersion() shortcut
- because(reason) for insight
- no rule => hard failure
basics
~10 sUse resolutionStrategy.capabilitiesResolution.withCapability('group:name') { select(...) } inside a configuration. The closure receives the conflicting candidates and you call select() to pick one, or selectHighestVersion() to take the newest.
solid answer
~40 sYou hook into `resolutionStrategy.capabilitiesResolution`. For a given capability you register a rule: ```kotlin configurations.all { resolutionStrategy.capabilitiesResolution.withCapability("org.slf4j:slf4j-logging") { val pick = candidates.firstOrNull { it.id.let { id -> id is ModuleComponentIdentifier && id.module == "slf4j-jdk14" } } if (pick != null) select(pick) else selectHighestVersion() } } ``` The rule's `candidates` list contains every variant claiming that capability. You call **`select(candidate)`** to choose the winner, optionally with a `because("reason")`, or **`selectHighestVersion()`** as a shortcut. Candidates not selected are dropped from the graph. If you register no rule, the conflict is a hard build failure. The rule runs at resolution time per configuration, so `configurations.all` (or a convention plugin) applies it everywhere consistently.
code
kotlin · 14 linesconfigurations.all {
resolutionStrategy.capabilitiesResolution.withCapability("log4j:log4j") {
// prefer the slf4j bridge over real log4j
val bridge = candidates.firstOrNull {
(it.id as? ModuleComponentIdentifier)?.module == "log4j-over-slf4j"
}
if (bridge != null) {
select(bridge)
because("route legacy log4j calls through slf4j")
} else {
selectHighestVersion()
}
}
}go deeper
Know that there's a withCapability rule where you select() the winner; details optional.
Write a basic rule using selectHighestVersion() and explain candidates/select.
Inspect candidate ids to select a specific module, add because(), and place the rule in configurations.all.
Centralize capability resolution in convention plugins as org policy and reason about cross-module version comparability.
## The resolution hook Capability conflicts are resolved through `ResolutionStrategy.getCapabilitiesResolution()`, reached via `configurations.<name>.resolutionStrategy.capabilitiesResolution`. You register a rule for a specific capability coordinate: ```kotlin configurations.all { resolutionStrategy.capabilitiesResolution.withCapability("com.google.collections:google-collections") { // 'this' is CapabilityResolutionDetails selectHighestVersion() } } ``` Note the capability you pass to `withCapability` is the **shared capability coordinate** (group:name, version optional), not necessarily a module GAV — it is whatever capability the conflicting variants declare. ## Inside the rule: CapabilityResolutionDetails The rule receives a `CapabilityResolutionDetails` with: - **`getCandidates()`** — the list of `CapabilityResolutionDetails.Candidate`/component candidates competing for this capability. Each exposes its `getId()` (a `ComponentIdentifier`, usually a `ModuleComponentIdentifier` with group/module/version) and variant name. - **`select(candidate)`** — choose this candidate as the winner; the others are evicted. - **`selectHighestVersion()`** — convenience: pick the candidate with the highest version. - **`because(reason)`** — attach a human-readable reason that shows up in `dependencyInsight`/build scans. ## A realistic pick ```kotlin configurations.all { resolutionStrategy.capabilitiesResolution.withCapability("org.slf4j:slf4j-logging") { candidates.find { (it.id as? ModuleComponentIdentifier)?.module == "slf4j-simple" }?.let { select(it) } because("standardise on slf4j-simple for logging binding") } } ``` If your `find` returns null (the preferred module isn't a candidate here) you can fall back to `selectHighestVersion()` so you never leave the conflict unresolved. ## Where to put it - `configurations.all { ... }` applies to every configuration in the project. - Better for multi-project builds: put it in a **convention/precompiled plugin** applied to all subprojects, so the policy is centralized. ## What if you don't resolve it The default is a **hard failure** with a message listing the conflicting modules and the shared capability. Gradle will not silently pick one — that's the whole point: mutually-exclusive features must be a deliberate decision. ## Relationship to declaring the capability Resolution only fires once a conflict *exists*. For third-party modules that don't already declare a shared capability, you must first **declare** it via a component metadata rule (`withCapabilities { addCapability(...) }`). Resolution and declaration are two halves of the same workflow.
- What does the rule's candidates list contain?Every variant/component claiming the conflicting capability, each with an id (ComponentIdentifier) you can inspect to decide which to select().
- Difference between select() and selectHighestVersion()?select() picks a specific candidate you choose explicitly; selectHighestVersion() is a convenience that auto-picks the highest-versioned candidate.
- Why apply it with configurations.all or a convention plugin?So the resolution policy is consistent across every configuration and every subproject, rather than per-configuration and easy to forget.
saying these in an interview costs you the question
- Calling resolutionStrategy.force(...) — that's version forcing, a sibling concern, not capability resolution.
- Thinking selectHighestVersion makes semantic sense across two *different* modules — versions aren't comparable across unrelated modules, so usually you select() explicitly.
- Assuming the conflict auto-resolves to 'highest' like a version conflict — it fails hard until you write a rule.