skip to content

How would you programmatically determine why Gradle selected a particular version of a dependency, using the ResolutionResult API?

level: seniorimportance: should knowfreq 35%

answer

  1. ResolvedComponentResult.selectionReason
  2. ComponentSelectionCause enum
  3. requested vs selected on the edge
  4. traverse from rr.root
  5. dependencyInsight uses this

basics

~10 s

Walk the graph via incoming.resolutionResult.allComponents, find the ResolvedComponentResult for the module, and read its selectionReason — its descriptors explain the cause (conflict resolution, constraint, forced, etc.).

solid answer

~40 s

Each node in the graph is a `ResolvedComponentResult`, and it carries a `ComponentSelectionReason` accessible via `selectionReason`. That reason is a list of `ComponentSelectionDescriptor`s, each with a `cause` (an enum: `REQUESTED`, `CONFLICT_RESOLUTION`, `CONSTRAINT`, `FORCED`, `SELECTED_BY_RULE`, etc.) and a human-readable `description`. To answer 'why this version?', iterate `resolutionResult.allComponents`, match by `moduleVersion.module`, then inspect `selectionReason.descriptions`. This is exactly what the built-in `dependencyInsight` task does. You can also start from `resolutionResult.root` and traverse `dependencies` to reconstruct the full edge structure and see *which requesting dependency* pulled in or constrained the version, since each `ResolvedDependencyResult` exposes both `requested` and `selected`. Do this work at execution time to avoid forcing eager resolution.

code

kotlin · 13 lines
kotlin
tasks.register("whyGuava") {
    val rr = configurations.getByName("runtimeClasspath").incoming.resolutionResult
    doLast {
        rr.allComponents {
            if (moduleVersion?.name == "guava") {
                println("Selected ${'$'}{moduleVersion}")
                selectionReason.descriptions.forEach { d ->
                    println("  ${'$'}{d.cause}: ${'$'}{d.description}")
                }
            }
        }
    }
}

go deeper

for a junior

Know that the selected version and its reason are recorded on each component and surfaced by dependencyInsight.

for a middle

Use allComponents + selectionReason.descriptions to report the cause for a module.

for a senior

Traverse from root to relate requested vs selected per edge and enumerate ComponentSelectionCause values; do it at execution time.

for a principal

Build governance gates on this (fail on forbidden/forced versions) and reason about config-cache-safe extraction of the graph into a serializable model.

## The goal Given a resolved configuration, answer: *why did module X end up at version Y?* The data lives entirely in the `ResolutionResult` graph — no artifact download required. ## The relevant types - **`ResolvedComponentResult`** (a node): `id`, `moduleVersion` (`ModuleVersionIdentifier` = group/module/version), `selectionReason`, `variants`, and `getDependencies()` (outgoing edges). - **`ComponentSelectionReason`**: `getDescriptions()` → `List<ComponentSelectionDescriptor>`; helper booleans like `isConflictResolution()`, `isConstrained()`, `isForced()`. - **`ComponentSelectionDescriptor`**: `getCause()` (a `ComponentSelectionCause` enum) and `getDescription()` (free text, e.g. 'between versions 1.2 and 1.5'). - **`ComponentSelectionCause`** values: `REQUESTED`, `SELECTED_BY_RULE`, `FORCED`, `CONFLICT_RESOLUTION`, `COMPOSITE_BUILD`, `REJECTION`, `CONSTRAINT`, `BY_ANCESTOR`. ## Two traversal styles **Flat callback** — quickest for 'find this one module': ```kotlin val rr = configurations.getByName("runtimeClasspath").incoming.resolutionResult rr.allComponents { if (moduleVersion?.name == "guava") { selectionReason.descriptions.forEach { println("${'$'}{it.cause}: ${'$'}{it.description}") } } } ``` **Structured traversal** — from the root to see *who requested what*: ```kotlin fun visit(node: ResolvedComponentResult, seen: MutableSet<ResolvedComponentResult>) { if (!seen.add(node)) return node.dependencies.filterIsInstance<ResolvedDependencyResult>().forEach { edge -> println("${'$'}{node.id} requested ${'$'}{edge.requested} -> got ${'$'}{edge.selected.id}") visit(edge.selected, seen) } } visit(rr.root, mutableSetOf()) ``` The `requested` vs `selected` distinction is what reveals a version *upgrade* (requested 1.2, selected 1.5 via conflict resolution) or a *constraint* forcing it down. ## Why dependencyInsight is the canonical example The `dependencyInsight --dependency guava --configuration runtimeClasspath` report is built directly on this API; reproducing it programmatically is a common build-logic task (e.g. failing the build if a forbidden version was selected, or emitting a custom report). ## Performance / config cache Accessing `resolutionResult` resolves the configuration. Do it inside a `doLast`/task action (execution time), not during configuration, and prefer reading into a serializable model if you need configuration-cache compatibility — `ResolutionResult` itself shouldn't be held across the configuration boundary.

  • What is the difference between the requested and selected values on a ResolvedDependencyResult?
    requested is the version (or range/constraint) the edge asked for; selected is the actual ResolvedComponentResult Gradle ended up with after conflict resolution, constraints, and rules — comparing them reveals upgrades/downgrades.
  • Which ComponentSelectionCause indicates the version came from a dependency constraint rather than a direct request?
    CONSTRAINT. Causes like CONFLICT_RESOLUTION, FORCED, and SELECTED_BY_RULE distinguish the other mechanisms that can drive selection.
  • Why traverse from root rather than only using allComponents?
    allComponents is a flat set of nodes; traversing from root via dependencies reconstructs the edges so you can see which requesting module pulled in or constrained a given version.

saying these in an interview costs you the question

  • Confusing selectionReason (graph metadata) with the artifact's file or classifier.
  • Reading allComponents at configuration time and forcing eager resolution on every build.
  • Assuming requested always equals selected — they differ whenever conflict resolution or constraints act.

context