skip to content

How can a Component Metadata Rule use capabilities to resolve a 'same library, different coordinates' conflict?

level: seniorimportance: should knowfreq 35%

answer

  1. capability = group:name:version token
  2. default capability = own coords
  3. addCapability so both share it
  4. Gradle raises capability conflict
  5. capabilitiesResolution.selectHighestVersion

basics

~10 s

Declare that two equivalent modules provide the same capability (e.g. give old google-collections Guava's capability). Gradle then detects the conflict and you resolve it by choosing one module, avoiding duplicate classes on the classpath.

solid answer

~40 s

A **capability** in Gradle is a `group:name:version` token that says "this variant provides this thing"; by default a module provides a capability matching its own coordinates. Two libraries can be the *same* thing under different coordinates — classically `com.google.collections:google-collections` and `com.google.guava:guava`. Their default capabilities differ, so Gradle puts both on the classpath and you get duplicate/clashing classes. A metadata rule fixes this: in a `withModule` rule on `google-collections`, call `details.allVariants { withCapabilities { addCapability("com.google.guava", "guava", version) } }`. Now both modules declare the same capability, Gradle detects a **capability conflict**, and resolution fails until you pick a winner — which you do with a `resolutionStrategy.capabilitiesResolution.withCapability(...) { selectHighestVersion() }` or by selecting Guava explicitly. The result: a clean classpath with one implementation. This generalizes to `slf4j` vs `log4j-over-slf4j`, `findbugs` vs `jsr305`, logging-bridge mutual exclusion, etc.

code

kotlin · 11 lines
kotlin
dependencies {
    components {
        withModule("com.google.collections:google-collections", GuavaRule::class)
    }
}

configurations.all {
    resolutionStrategy.capabilitiesResolution.withCapability("com.google.guava:guava") {
        selectHighestVersion()
    }
}

go deeper

for a junior

Know the symptom (duplicate classes from two coordinates of the same lib) exists; not expected to wire the rule.

for a middle

Explain that a default capability equals the coordinates and that adding a shared capability surfaces a conflict.

for a senior

Write the withCapabilities/addCapability rule plus capabilitiesResolution policy and name real cases (guava, logging bridges).

for a principal

Standardize capability normalization in a shared plugin so the whole org avoids duplicate-library classpath bugs; define org-wide resolution policy.

## What a capability is Gradle's resolution is **variant-aware**: each module exposes variants, and each variant declares one or more **capabilities** — identifiers of the form `group:name:version`. A variant *provides* a capability; a graph may contain at most one variant providing a given capability (besides version). By default a module `g:a:v` provides exactly one capability `g:a:v`. This is how Gradle knows two unrelated jars are distinct: different capabilities, both allowed. ## The 'same library, two coordinates' bug Guava was once published as `com.google.collections:google-collections`, then moved to `com.google.guava:guava`. They contain overlapping classes (`com.google.common.*`). If a transitive graph drags in both, Gradle sees two different capabilities (`...:google-collections` and `...:guava`) and happily keeps both — producing duplicate classes, `NoSuchMethodError`s, or non-deterministic class loading. ## Fixing it with a metadata rule Teach the legacy module to declare the *modern* capability: ```kotlin @CacheableRule abstract class GoogleCollectionsRule : ComponentMetadataRule { override fun execute(context: ComponentMetadataContext) { context.details.allVariants { withCapabilities { // legacy module now ALSO provides guava's capability addCapability("com.google.guava", "guava", "1.0") } } } } dependencies { components { withModule("com.google.collections:google-collections", GoogleCollectionsRule::class) } } ``` Now both modules provide `com.google.guava:guava` — Gradle raises a **capability conflict** because two variants claim the same capability. ## Resolving the conflict A conflict isn't an error you ignore — you must choose a winner: ```kotlin configurations.all { resolutionStrategy.capabilitiesResolution.withCapability("com.google.guava:guava") { selectHighestVersion() // or select(...) a specific candidate } } ``` Gradle then keeps one module and evicts the other, leaving a single, consistent set of classes. ## Where else this applies - **Logging bridges**: `log4j:log4j` vs `org.slf4j:log4j-over-slf4j` are mutually exclusive — give them the same capability so only one survives. - **Annotation jars**: `com.google.code.findbugs:jsr305` vs `annotations`. - **Internal forks**: an internal patched fork and the upstream artifact. Capabilities are strictly more expressive than `exclude`/`force`: they let Gradle *reason* about equivalence and report a clear conflict, instead of you silently dropping one coordinate everywhere it appears.

  • After adding the capability, why does the build now fail until you act?
    Two variants providing the same capability is a conflict Gradle refuses to resolve silently. You must declare a resolution strategy (select highest, or pick one explicitly) so it can keep exactly one.
  • How is this better than excluding `google-collections` everywhere?
    Excludes are per-consumer and easy to miss; capabilities make the equivalence first-class so Gradle detects the clash anywhere in the graph and applies one central resolution policy, and the choice is explicit and auditable.
  • What's the default capability of a module?
    Its own coordinates, `group:name:version`. That's why two distinct coordinates for the same library aren't auto-detected without a metadata rule.

Two products with different SKUs that are actually the same item. Until you tag them with the same product code (capability), the warehouse stocks both; once they share a code, the system flags the duplicate and you keep one.

saying these in an interview costs you the question

  • Thinking `addCapability` alone fixes it — without a `capabilitiesResolution` policy the build fails on the conflict.
  • Confusing capabilities (equivalence/conflict) with attributes (variant selection).

context