How would you programmatically determine why Gradle selected a particular version of a dependency, using the ResolutionResult API?
answer
- ResolvedComponentResult.selectionReason
- ComponentSelectionCause enum
- requested vs selected on the edge
- traverse from rr.root
- dependencyInsight uses this
basics
~10 sWalk 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 sEach 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 linestasks.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
Know that the selected version and its reason are recorded on each component and surfaced by dependencyInsight.
Use allComponents + selectionReason.descriptions to report the cause for a module.
Traverse from root to relate requested vs selected per edge and enumerate ComponentSelectionCause values; do it at execution time.
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.