skip to content

Capabilities and Conflicts

Capability conflicts between mutually exclusive modules and how a rule picks the winning variant. Interviewers reach for the classic case of two coordinates providing the same API and both landing on the classpath.

on this pageshow

questions

5

What is a 'capability' in Gradle dependency management, and what problem does it solve compared to plain version conflict resolution?

level: middleimportance: must knowfreq 45%

answer

  1. group:name:version identity of a variant
  2. implicit capability = the module's own GAV
  3. two different modules, same feature
  4. guava vs google-collections, slf4j shims
  5. conflict = hard failure by default

basics

~20 s

A capability is a label (group:name:version) saying 'I provide this feature'. By default a module provides one capability matching its GAV. Two modules offering the same capability conflict, even if they are different modules — like log4j and log4j-over-slf4j both providing logging.

solid answer

~40 s

A **capability** is an identity Gradle attaches to a variant, of the form `group:name:version`, that declares *what feature* it provides — independent of the module's own coordinates. Every component implicitly declares one capability equal to its GAV. Plain conflict resolution only deduplicates **different versions of the same module**; it cannot detect that two *different* modules implement the *same* thing (e.g. `com.google.guava:guava` vs `com.google.collections:google-collections`, or `slf4j` vs `log4j-over-slf4j`). When you declare that both modules provide the same capability, Gradle detects them as **mutually exclusive** and fails the build with a capability conflict, forcing you to choose one. This turns a silent classpath clash (duplicate classes, `LinkageError` at runtime) into an early, explicit resolution decision.

code

kotlin · 10 lines
kotlin
dependencies {
    // teach Gradle that these two distinct modules are the same feature
    components.withModule("com.google.collections:google-collections") {
        allVariants {
            withCapabilities {
                addCapability("com.google.guava", "guava", "1.0")
            }
        }
    }
}

go deeper

for a junior

Know that a capability is a label for 'what a module provides' and that two modules claiming the same capability conflict.

for a middle

Explain implicit GAV capability, give a real example (logging shims, guava), and contrast with plain version conflict resolution.

for a senior

Discuss adding capabilities via component metadata rules and why conflicts are failures by default.

for a principal

Frame capabilities as a governance tool to enforce one canonical implementation org-wide, codified in convention plugins.

## What a capability is In Gradle's dependency model, every resolved thing is a **variant** of a **component**. A **capability** is a coordinate of the form `group:name:version` that a variant declares to say "I provide *this* feature." It is orthogonal to the component's own GAV (group:artifact:version). By default, every component declares exactly **one implicit capability** equal to its own GAV. So `org.apache.commons:commons-lang3:3.12.0` provides the capability `org.apache.commons:commons-lang3:3.12.0`. ## The problem it solves Default conflict resolution answers the question *"the graph wants two versions of the same module — which version wins?"* (highest version by default). But it is blind to a different problem: **two distinct modules that are really the same feature**. Classic examples: - `com.google.collections:google-collections` and `com.google.guava:guava` (Guava absorbed google-collections). - `org.slf4j:log4j-over-slf4j` and `log4j:log4j` (one is a drop-in replacement for the other). - `javax.*` vs `jakarta.*` of the same API. Without capabilities, both end up on the classpath. You get duplicate classes and, at runtime, `NoSuchMethodError`/`LinkageError`/non-deterministic class loading. ## How capabilities fix it If you make both modules declare the **same capability** (e.g. both declare `org.apache.logging:logging:1.0`), Gradle sees two variants offering one capability in the same resolution and raises a **capability conflict** — by default a hard failure. You then resolve it explicitly, choosing a winner. There are two places this happens: 1. **Component metadata rules** — you *add* a capability to third-party modules that don't declare it (e.g. tell Gradle that both logging shims provide the same capability). 2. **Resolution strategy** — `configurations.all { resolutionStrategy.capabilitiesResolution.withCapability(...) { selectHighestVersion() / select(...) } }` to pick the winner. ## Why it matters It converts a **silent runtime hazard** into a **build-time, explicit decision**. The cost is that conflicts are failures by default, so you must wire up resolution. ```kotlin dependencies { components.all { // declares both modules provide one shared capability if (id.group == "log4j" && id.name == "log4j") { allVariants { withCapabilities { addCapability("org.slf4j", "slf4j-logging", "1.0") } } } } } ```

  • What capability does a component declare if you do nothing?
    Exactly one implicit capability equal to its own GAV (group:artifact:version).
  • Why is a capability conflict a failure by default rather than 'pick highest'?
    Because two modules with the same capability are usually mutually exclusive (duplicate classes); silently picking one could break the build, so Gradle forces an explicit choice.

Version conflict resolution is choosing between two editions of the same book. A capability conflict is realising two differently-titled books are actually the same book — and you can only keep one on the shelf.

saying these in an interview costs you the question

  • Saying capabilities are just another name for the module version — they are an independent feature identity.
  • Claiming Gradle auto-detects guava vs google-collections out of the box — it does not; you must declare the shared capability.

context

open as a page

How do you resolve a capability conflict programmatically in Gradle, picking one variant over another?

level: seniorimportance: must knowfreq 40%

basics

~10 s

Use 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.

open as a page

Your Gradle build fails with 'Cannot select module ... Multiple variants ... provide the same capability'. What does this mean and what are your options?

level: middleimportance: should knowfreq 38%

basics

~20 s

Two variants in your graph declare the same capability, so they're mutually exclusive and Gradle refuses to put both on the classpath. You must pick one — via a capabilitiesResolution rule, or by removing/substituting one of the modules.

open as a page

How do you make two third-party modules that don't declare any shared capability become mutually exclusive in Gradle?

level: seniorimportance: should knowfreq 30%

basics

~20 s

Add a component metadata rule that attaches the same capability to both modules: components.withModule(...) { allVariants { withCapabilities { addCapability(group, name, version) } } }. Once both share a capability, Gradle treats them as conflicting.

open as a page

How do you publish a component so that two of its optional features are mutually exclusive via capabilities, and what's the difference between feature variants and a capability conflict?

level: principalimportance: nice to knowfreq 18%

basics

~20 s

When publishing, declare a capability on a variant via the Java library's registerFeature or by adding capabilities to outgoing configurations. If you give two alternative implementations the same capability, consumers can pick only one — Gradle enforces mutual exclusivity at resolution.

open as a page