# Graph Resolution

version 9.8.0-20260812010846+0000

During the graph resolution phase, Gradle constructs a resolved dependency graph, which models the relationships between different components and their variants.

## Modules

Graph resolution begins with dependencies declared in the build script:

build.gradle.kts

    dependencies {
        implementation("com.fasterxml.jackson.core:jackson-databind:2.17.2")
    }

In the Learning the Basics section, we learned that a module is a published unit of work (like a library or application), while a dependency is a reference to a module required for a project to compile or run.

In the example above, the module is `com.fasterxml.jackson.core:jackson-databind`.

## Components

Each version of a module is referred to as a component.

In the example above for the `com.fasterxml.jackson.core:jackson-databind:2.17.2` module, `2.17.2` is the version.

## Metadata

A component is detailed by metadata, which is available in the repository where the component is hosted as a `ivy`, `pom`, or `GMM` metadata.

Here’s a condensed sample of the metadata for `com.fasterxml.jackson.core:jackson-databind:2.17.2`:

jackson-databind-2.17.2.module

    {
      "formatVersion": "1.1",
      "component": {
        "group": "com.fasterxml.jackson.core",
        "module": "jackson-databind",
        "version": "2.17.2"
      },
      "variants": [
        {
          "name": "apiElements",
          ...
        },
        {
          "name": "runtimeElements",
          "attributes": {
            "org.gradle.category": "library",
            "org.gradle.dependency.bundling": "external",
            "org.gradle.libraryelements": "jar",
            "org.gradle.usage": "java-runtime"
          },
          "dependencies": [
            {
              "group": "com.fasterxml.jackson.core",
              "module": "jackson-annotations",
              "version": {
                "requires": "2.17.2"
              }
            }
          ],
          "files": [
            {
              "name": "jackson-databind-2.17.2.jar"
            }
          ]
        }
      ]
    }

Some items in the file should be familiar to you such as "files", "dependencies", "components", "module" and "version". Let’s focus on the variants provided in the metadata.

## Variants

A variant is a specific variation of a component tailored for a particular use case or environment.

Variants allow you to provide different definitions of your component depending on the context in which it’s used.

As detailed above, the metadata of the `com.fasterxml.jackson.core:jackson-databind:2.17.2` component offers two variants:

- The `apiElements` variant includes dependencies required for compiling projects against Jackson Databind.

- The `runtimeElements` variant includes dependencies required for executing Jackson Databind during runtime.

Variant | Dependencies | Artifacts
--- | --- | ---
`apiElements` | `com.fasterxml.jackson.core`, `com.fasterxml.jackson.bom` | `jackson-databind-2.17.2.jar`
`runtimeElements` | `com.fasterxml.jackson.core` | `jackson-databind-2.17.2.jar`
…​ other variant …​ | …​ some dependencies …​ | …​ some artifacts …​

Each variant consists of a set of artifacts and defines a set of dependencies (i.e. seen as transitive dependencies of the build):

- The `runtimeElements` variant of `com.fasterxml.jackson.core:jackson-databind:2.17.2`:

  - Depends on `com.fasterxml.jackson.core`.

  - Provides an artifact called `jackson-databind-2.17.2.jar`.

To differentiate between the `apiElements` and `runtimeElements` variants, Gradle uses attributes.

## Attributes

To differentiate between variants, Gradle uses attributes.

Attributes are used to define specific characteristics or properties of variants and the context in which those variants should be used.

In the metadata for Jackson Databind, we see that the `runtimeElements` variant is described by the `org.gradle.category`, `org.gradle.dependency.bundling`, `org.gradle.libraryelement`, and `org.gradle.usage` attributes:

    {
      "variants": [
        {
          "name": "runtimeElements",
          "attributes": {
            "org.gradle.category": "library",
            "org.gradle.dependency.bundling": "external",
            "org.gradle.libraryelements": "jar",
            "org.gradle.usage": "java-runtime"
          }
        }
      ]
    }

Attributes are defined as `key:value` pairs, such as `org.gradle.category": "library"`.

Now that we understand the building blocks for dependency management, let’s look into graph resolution.

## Dependency Graph

Gradle builds a dependency graph that represents a configuration’s dependencies and their relationships. This graph includes both direct and transitive dependencies.

The graph is made up of nodes where each node represents a variant. These nodes are connected by edges, representing the dependencies between variants. The edges indicate how one variant relies on another.

The `dependencies` task can be used to partially visualize the structure of a dependency graph:

    $ ./gradlew app:dependencies

    ...
    runtimeClasspath - Runtime classpath of source set 'main'.
    \--- com.fasterxml.jackson.core:jackson-databind:2.17.2
         +--- com.fasterxml.jackson.core:jackson-annotations:2.17.2
         |    \--- com.fasterxml.jackson:jackson-bom:2.17.2
         |         +--- com.fasterxml.jackson.core:jackson-annotations:2.17.2
         |         +--- com.fasterxml.jackson.core:jackson-core:2.17.2
         |         \--- com.fasterxml.jackson.core:jackson-databind:2.17.2
         +--- com.fasterxml.jackson.core:jackson-core:2.17.2
         |    \--- com.fasterxml.jackson:jackson-bom:2.17.2
         \--- com.fasterxml.jackson:jackson-bom:2.17.2

In this truncated output, `runtimeClasspath` represents the specific resolvable configurations in the project.

Each resolvable configuration calculates a separate dependency graph. That’s because different configurations can resolve to a different set of transitive dependencies for the same set of declared dependencies.

In the example above, the resolvable configuration, `compileClasspath` could resolve a different set of dependencies and produce a very different graph than `runtimeClasspath`.

So how does Gradle build the dependency graph?

## Graph Resolution Flow

Graph resolution operates on a node-by-node (i.e., variant-by-variant) basis.

Each iteration of the loop processes one node at a time, starting by de-queuing a node.

In the beginning, the queue is empty. When the process starts, a root node added to the queue. The root node is effectively the resolvable configuration:

Gradle starts the loop by pulling the root node from the queue. Gradle examines the dependencies of the root node, resolves their conflicts, and downloads their metadata. Based on their metadata, Gradle selects variants of those dependencies and adds them back to the queue.

| The root variant’s dependencies correspond to the declared dependencies of the resolvable configuration.

At this point, the queue contains all selected variants for the root node’s dependencies, which will now be processed one by one.

For each node in the loop Gradle:

1. Evaluates its dependencies.

2. Determines their targeted versions using conflict resolution.

3. Downloads the metadata for all components at once.

4. Select variants for each of those components.

5. Adds the variants to the top-level queue.

The loop is repeated until the node queue is empty. Once the process is complete, the dependency graph is resolved.

| Graph resolution alternates between parallel metadata downloads and single-threaded logic, repeating this pattern over and over again for a single graph.

## Conflict Resolution

When performing dependency resolution, Gradle handles two types of conflicts:

1. Version conflicts: Which occur when multiple dependencies request the same dependency but with different versions. Gradle must choose which version to include in the graph.

2. Implementation / Capability conflicts: Which occur when the dependency graph contains different modules that provide the same functionality or capability. Gradle resolves these by selecting one module to avoid duplicate implementations.

The dependency resolution process is highly customizable and many APIs can influence the process.

### A. Version conflicts

A version conflict can occur when two components:

- Depend on the same module, such as `com.google.guava:guava`

- But on different versions, for example, `20.0` and `25.1-android`:

  - Our project directly depends on `com.google.guava:guava:20.0`

  - Our project also depends on `com.google.inject:guice:4.2.2`, which in turn depends on `com.google.guava:guava:25.1-android`

Gradle must resolve this conflict by selecting one version to include in the dependency graph.

Gradle considers all requested versions across the dependency graph and, by default, selects the highest version. Detailed version ordering is explained in version ordering.

Gradle also supports the concept of rich version declarations, which means that what constitutes the "highest" version depends on how the versions were declared:

- Without ranges: The highest non-rejected version will be selected.

  - If a `strictly` version is declared that is lower than the highest, resolution will fail.

- With ranges:

  - If a non-range version fits within the range or is higher than the upper bound, it will be selected.

  - If only ranges exist, the selection depends on the intersection of those ranges:

    - If ranges overlap, the highest existing version in the intersection is selected.

    - If no clear intersection exists, the highest version from the largest range will be selected. If no version exists in the highest range, the resolution fails.

  - If a `strictly` version is declared that is lower than the highest, resolution will fail.

For version ranges, Gradle needs to perform intermediate metadata lookups to determine what variations are available, as explained in Metadata Retrieval.

#### Versions with qualifiers

The term "qualifier" refers to the portion of a version string that comes after a non-dot separator, like a hyphen or underscore.

For example:

Original version | Base version | Qualifier
--- | --- | ---
1.2.3 | 1.2.3 | <none>
1.2-3 | 1.2 | 3
1_alpha | 1 | alpha
abc | abc | <none>
1.2b3 | 1.2 | b3
abc.1+3 | abc.1 | 3
b1-2-3.3 | b | 1-2-3.3

As you can see separators are any of the `.`, `-`, `_`, `+` characters, plus the empty string when a numeric and a non-numeric part of the version are next to each-other.

By default, Gradle gives preference to versions without qualifiers when resolving conflicts.

For example, in version `1.0-beta`, the base form is `1.0`, and `beta` is the qualifier. Versions without qualifiers are considered more stable, so Gradle will prioritize them.

Here are a few examples to clarify:

- `1.0.0` (no qualifier)

- `1.0.0-beta` (qualifier: `beta`)

- `2.1-rc1` (qualifier: `rc1`)

Even if the qualifier is lexicographically higher, Gradle will typically consider a version like `1.0.0` higher than `1.0.0-beta`.

When resolving conflicts between versions, Gradle applies the following logic:

1. Base version comparison: Gradle first selects versions with the highest base version, ignoring any qualifiers. All others are discarded.

2. Qualifier handling: If there are still multiple versions with the same base version, Gradle picks one with a preference for versions without qualifiers (i.e., release versions). If all versions have qualifiers, Gradle will consider the qualifier’s order, preferring more stable ones like "release" over others such as "beta" or "alpha."

### B. Implementation / Capability conflicts

Conflicts arise in the following scenarios:

- Incompatible variants: When two modules attempt to select different, incompatible variants of a dependency.

- Same capability: When multiple modules declare the same capability, creating an overlap in functionality.

This type of conflict is resolved during Variant Selection described below.

## Metadata Retrieval

Gradle requires module metadata in the dependency graph for two reasons:

1. Determining existing versions for dynamic dependencies: When a dynamic version (like `1.+` or `latest.release`) is specified, Gradle must identify the concrete versions available.

2. Resolving module dependencies for a specific version: Gradle retrieves the dependencies associated with a module based on the specified version, ensuring the correct transitive dependencies are included in the build.

### A. Determining existing versions for dynamic dependencies

When faced with a dynamic version, Gradle must identify the available concrete versions. To do that, Gradle checks each defined repository in the order they were added. It doesn’t stop at the first one that returns metadata but continues through all available repositories.

For Maven repositories, Gradle retrieves version information from the `maven-metadata.xml` file, which lists available versions.

For Ivy repositories, Gradle resorts to a directory listing to gather available versions.

The result is a list of candidate versions that Gradle evaluates and matches to the dynamic version. Gradle caches this information to optimize future resolution. At this point, version conflict resolution is resumed.

### B. Resolving module dependencies for a specific version

When Gradle tries to resolve a required dependency with a specific version, it follows this process:

1. Repository inspection: Gradle checks each repository in the order they are defined.

   - It looks for metadata files describing the module (`.module`, `.pom`, or `ivy.xml`), or directly for artifact files.

   - Modules with metadata files (`.module`, `.pom`, or `ivy.xml`) are prioritized over those with just an artifact file.

   - Once metadata is found in a repository, subsequent repositories are ignored.

2. Retrieving and parsing metadata: If metadata is found, it is parsed.

   - If the POM file has a parent POM, Gradle recursively resolves each parent module.

3. Requesting artifacts: All artifacts for the module are fetched from the same repository that provided the metadata.

4. Caching: All data, including the repository source and any potential misses, are stored in the dependency cache for future use.

| The point above highlights a potential issue with integrating Maven Local. Since Maven Local acts as a Maven cache, it may occasionally miss artifacts for a module. When Gradle sources a module from Maven Local and artifacts are missing, it assumes those artifacts are entirely unavailable.

### Repository disabling

When Gradle fails to retrieve information from a repository after multiple retries or specific fatal errors, the repository will no longer be checked for the remainder of the build. This will usually fail dependency resolution because Gradle will not continue to the next repository in the list unless told otherwise.

This behavior ensures reproducibility.

If the build were to continue while ignoring the faulty repository, subsequent builds could produce different results once the repository is back online.

Reasons for disabling a repository include:

- Unknown host errors

- Reaching max retry attempts on transient connection failures, like a server failing to respond

- Getting HTTP 5xx responses from the server

If your build setup relies on a repository that may be temporarily unavailable, see `UrlArtifactRepository.getAllowInsecureContinueWhenDisabled()` to allow Gradle to continue using other repositories even when that one encounters issues.

#### HTTP Retries

Gradle will attempt to connect to a repository multiple times before disabling it. If the connection fails, Gradle retries on specific errors that might be temporary, with increasing wait times between retries.

A repository is marked as unavailable when it cannot be reached, either due to a permanent error or after the maximum number of retries has been exhausted.

## Variant Selection

Based on the requirements of the build, Gradle selects one of the variants of the module present in the metadata.

Specifically, Gradle attempts to match the attributes from the resolved configuration to those in the module metadata.

Variant selection and attribute matching is fully described in the next section.

## Available APIs

The `ResolutionResult` API provides access to the resolved dependency graph without triggering artifact downloads.

The graph itself focuses on component variants, not the artifacts (files) associated with those variants:

- `ResolvedComponentResult` - Represents a resolved component in the raw dependency graph.

- `ResolvedVariantResult` - Represents a resolved variant of a component in the raw dependency graph.

Raw access to the dependency graph can be useful for a number of use cases:

- Visualizing the dependency graph, for example generating a `.dot` file for Graphviz.

- Exposing diagnostics about a given resolution, similar to the `dependencies` or `dependencyInsight` tasks.

- Resolving a subset of the artifacts for a dependency graph when used in conjunction with the `ArtifactView` API.

Consider the following function that traverses a dependency graph, starting from the root node. Callbacks are notified for each node and edge in the graph. This function can be used as a base for any use case that requires traversing a dependency graph:

build.gradle.kts

    fun traverseGraph(
        rootComponent: ResolvedComponentResult,
        rootVariant: ResolvedVariantResult,
        nodeCallback: (ResolvedVariantResult) -> Unit,
        edgeCallback: (ResolvedVariantResult, ResolvedVariantResult) -> Unit
    ) {
        val seen = mutableSetOf<ResolvedVariantResult>(rootVariant)
        nodeCallback(rootVariant)

        val queue = ArrayDeque(listOf(rootVariant to rootComponent))
        while (queue.isNotEmpty()) {
            val (variant, component) = queue.removeFirst()

            // Traverse this variant's dependencies
            component.getDependenciesForVariant(variant).forEach { dependency ->
                val resolved = when (dependency) {
                    is ResolvedDependencyResult -> dependency
                    is UnresolvedDependencyResult -> throw dependency.failure
                    else -> throw AssertionError("Unknown dependency type: $dependency")
                }
                if (!resolved.isConstraint) {
                    val toVariant = resolved.resolvedVariant

                    if (seen.add(toVariant)) {
                        nodeCallback(toVariant)
                        queue.addLast(toVariant to resolved.selected)
                    }

                    edgeCallback(variant, toVariant)
                }
            }
        }
    }

build.gradle

    void traverseGraph(
        ResolvedComponentResult rootComponent,
        ResolvedVariantResult rootVariant,
        Consumer<ResolvedVariantResult> nodeCallback,
        BiConsumer<ResolvedVariantResult, ResolvedVariantResult> edgeCallback
    ) {
        Set<ResolvedVariantResult> seen = new HashSet<>()
        seen.add(rootVariant)
        nodeCallback(rootVariant)

        def queue = new ArrayDeque<Tuple2<ResolvedVariantResult, ResolvedComponentResult>>()
        queue.add(new Tuple2(rootVariant, rootComponent))
        while (!queue.isEmpty()) {
            def entry = queue.removeFirst()
            def variant = entry.v1
            def component = entry.v2

            // Traverse this variant's dependencies
            component.getDependenciesForVariant(variant).each { dependency ->
                if (dependency instanceof UnresolvedDependencyResult) {
                    throw dependency.failure
                }
                if ((!dependency instanceof ResolvedDependencyResult)) {
                    throw new RuntimeException("Unknown dependency type: $dependency")
                }

                def resolved = dependency as ResolvedDependencyResult
                if (!dependency.constraint) {
                    def toVariant = resolved.resolvedVariant

                    if (seen.add(toVariant)) {
                        nodeCallback(toVariant)
                        queue.add(new Tuple2(toVariant, resolved.selected))
                    }

                    edgeCallback(variant, toVariant)
                }
            }
        }
    }

This function starts at the root variant, and performs a breadth-first traversal of the graph. The `ResolutionResult` API is lenient, so it is important to check whether a visited edge is unresolved (failed) or resolved. With this function, the node callback is always called before the edge callback for any given node.

Below, we leverage the above traversal function to transform a dependency graph into a `.dot` file for visualization:

build.gradle.kts

    abstract class GenerateDot : DefaultTask() {

        @get:Input
        abstract val rootComponent: Property<ResolvedComponentResult>

        @get:Input
        abstract val rootVariant: Property<ResolvedVariantResult>

        @TaskAction
        fun traverse() {
            println("digraph {")
            traverseGraph(
                rootComponent.get(),
                rootVariant.get(),
                { node -> println("    ${toNodeId(node)} [shape=box]") },
                { from, to -> println("    ${toNodeId(from)} -> ${toNodeId(to)}") }
            )
            println("}")
        }

        fun toNodeId(variant: ResolvedVariantResult): String {
            return "\"${variant.owner.displayName}:${variant.displayName}\""
        }
    }

build.gradle

    abstract class GenerateDot extends DefaultTask {

        @Input
        abstract Property<ResolvedComponentResult> getRootComponent()

        @Input
        abstract Property<ResolvedVariantResult> getRootVariant()

        @TaskAction
        void traverse() {
            println("digraph {")
            traverseGraph(
                rootComponent.get(),
                rootVariant.get(),
                node -> { println("    ${toNodeId(node)} [shape=box]") },
                (from, to) -> { println("    ${toNodeId(from)} -> ${toNodeId(to)}") }
            )
            println("}")
        }

        String toNodeId(ResolvedVariantResult variant) {
            return "\"${variant.owner.displayName}:${variant.displayName}\""
        }
    }

| A proper implementation would not use `println` but would write to an output file. For more details on declaring task inputs and outputs, see the Writing Tasks section.

When we register the task, we use the `ResolutionResult` API to access the root component and root variant of the `runtimeClasspath` configuration:

build.gradle.kts

    tasks.register<GenerateDot>("generateDot") {
        rootComponent = runtimeClasspath.flatMap {
            it.incoming.resolutionResult.rootComponent
        }
        rootVariant = runtimeClasspath.flatMap {
            it.incoming.resolutionResult.rootVariant
        }
    }

build.gradle

    tasks.register("generateDot", GenerateDot) {
        rootComponent = configurations.runtimeClasspath.incoming.resolutionResult.rootComponent
        rootVariant = configurations.runtimeClasspath.incoming.resolutionResult.rootVariant
    }

| This example uses incubating APIs.

`ResolutionResult` is Configuration Cache-compatible, so you can also wire it directly into a task input property if you need access to its full API at execution time.

Running this task, we get the following output:

    digraph {
        "root project 'example':runtimeClasspath" [shape=box]
        "com.google.guava:guava:33.2.1-jre:jreRuntimeElements" [shape=box]
        "root project 'example':runtimeClasspath" -> "com.google.guava:guava:33.2.1-jre:jreRuntimeElements"
        "com.google.guava:failureaccess:1.0.2:runtime" [shape=box]
        "com.google.guava:guava:33.2.1-jre:jreRuntimeElements" -> "com.google.guava:failureaccess:1.0.2:runtime"
        "com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava:runtime" [shape=box]
        "com.google.guava:guava:33.2.1-jre:jreRuntimeElements" -> "com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava:runtime"
        "com.google.code.findbugs:jsr305:3.0.2:runtime" [shape=box]
        "com.google.guava:guava:33.2.1-jre:jreRuntimeElements" -> "com.google.code.findbugs:jsr305:3.0.2:runtime"
        "org.checkerframework:checker-qual:3.42.0:runtimeElements" [shape=box]
        "com.google.guava:guava:33.2.1-jre:jreRuntimeElements" -> "org.checkerframework:checker-qual:3.42.0:runtimeElements"
        "com.google.errorprone:error_prone_annotations:2.26.1:runtime" [shape=box]
        "com.google.guava:guava:33.2.1-jre:jreRuntimeElements" -> "com.google.errorprone:error_prone_annotations:2.26.1:runtime"
    }

Compare this to the output of the `dependencies` task:

    runtimeClasspath
    \--- com.google.guava:guava:33.2.1-jre
         +--- com.google.guava:failureaccess:1.0.2
         +--- com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava
         +--- com.google.code.findbugs:jsr305:3.0.2
         +--- org.checkerframework:checker-qual:3.42.0
         \--- com.google.errorprone:error_prone_annotations:2.26.1

Notice how the graph is the same for both representations, the only difference it the chosen variant information available in the dot graph.

Next Step: Learn about Variant Selection and Attribute Matching >>