Gradle dependency tree

Gradle dependency tree

Last Updated on September 8, 2021

In Gradle dependencies are libraries required to build your code. Each of these libraries may have their own dependencies, adding transitive dependencies to your project. This structure is called the Gradle dependency tree, with its own rules on dependency conflict resolution and more.

In this article you’ll learn how to view the dependency tree, so you can understand fully how you project is built and resolve common issues.

Dependencies and dependency configurations

To be able to properly navigate the Gradle dependency tree, it’s important to understand how dependencies can be configured within different configurations.

  • a dependency is a specific library/artifact found in a remote repository such as Maven or JCenter
  • a dependency configuration is a grouping of dependencies. Common examples include implementation and testImplementation, both made available by the Java plugin.

By default Gradle doesn’t come with any dependency configurations, but they get added by plugins such as the Java plugin.

Here’s an example build.gradle snippet from a Gradle Java project. We’re adding dependencies for the guava and junit libraries.

dependencies {
    implementation 'com.google.guava:guava:30.1-jre'
    testImplementation 'junit:junit:4.10'
}

implementation is a configuration which has the guava library attached, and testImplementation is another configuration with the junit library attached. That’s important because these configurations are used by Gradle to generate the various classpaths for compiling and running production (non-test) and test classes.

Dependency configuration inheritance

Dependency configurations can inherit from each other. When a dependency configuration inherits from a parent configuration, it gets all the dependencies of the parent. That’s how the Java plugin calculates the compile-time and runtime classpaths, by inheriting from configurations against which you’ve declared dependencies.

Here’s a diagrams showing 7 dependency configurations added by the Java plugin, and their relationships.

This shows us that:

  • compileClasspath is created from the dependencies declared against the compileOnly and implementation dependency configurations
  • runtimeClasspath is created from the dependencies declared against the implementation and runtimeOnly dependency configurations

For a full list of dependency configurations check out the Java plugin docs.

Generating the dependency tree

Every Gradle project comes with a dependencies task which prints a dependency report, including the dependency tree.

Execute the task like this:

./gradlew dependencies

By default you’ll get a dependency tree for all dependency configurations. To see the dependency tree for a specific dependency configuration, pass the name of the dependency configuration:

./gradlew dependencies --configuration <configuration-name>

A simple example

Here’s a simple build.gradle representing a project with two dependencies declared within the implementation dependency configuration.

plugins {
    id 'java'
}

repositories {
    mavenCentral()
}

dependencies {
    implementation 'com.google.guava:guava:30.1-jre'
    implementation 'com.google.code.findbugs:jsr305:3.0.1'
}

So we have dependencies declared on the guava and findbugs jsr305 libraries.

Let’s say we want to inspect the dependency tree for the compileClasspath dependency configuration. We just run the dependencies Gradle task, like this:

$ ./gradlew dependencies --configuration compileClasspath

> Task :dependencies

------------------------------------------------------------
Root project
------------------------------------------------------------

compileClasspath - Compile classpath for source set 'main'.
+--- com.google.guava:guava:30.1-jre
|    +--- com.google.guava:failureaccess:1.0.1
|    +--- 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.5.0
|    +--- com.google.errorprone:error_prone_annotations:2.3.4
|    \--- com.google.j2objc:j2objc-annotations:1.3
\--- com.google.code.findbugs:jsr305:3.0.1 -> 3.0.2

(*) - dependencies omitted (listed previously)

Under compileClasspath is a simple tree structure, represented by the dependencies we declared in build.gradle, and any transitive dependencies.

  • the declared guava library has 6 transitive dependencies, which have each been added to this dependency configuration. Notice this includes the findbugs jsr305 library, with version 3.0.2.
  • the declared findbugs jsr305 library has no transitive dependencies. Notice how the declared version of 3.0.1 has been overridden with 3.0.2.

We just discovered that in this project the compileClasspath configuration has a more recent version of the findbugs jsr305 library than we declared, due to a transitive dependency of guava pulling in the more recent version. All thanks to the Gradle dependency tree. ✅

Dependency conflict resolution: whenever Gradle finds the same dependency declared multiple times with different versions, we have a conflict on our hands. In this case, Gradle picks the one with the most recent version.

Omitted dependencies

In the dependency tree shown above, you may have noticed at the end this message:

(*) – dependencies omitted (listed previously)

In order to keep the output as concise as possible, Gradle won’t print repeated sections of the dependency tree.

To illustrate this, let’s use an example project with a dependency on the spring-aop library, part of the popular Spring framework. Here’s how the build.gradle looks:

plugins {
    id 'java'
}

repositories {
    mavenCentral()
}

dependencies {
    compile 'org.springframework:spring-aop:5.3.4'
}

When we run the dependencies task on the compileClasspath dependency configuration, we get this output:

$ ./gradlew dependencies --configuration compileClasspath

> Task :dependencies

------------------------------------------------------------
Root project
------------------------------------------------------------

compileClasspath - Compile classpath for source set 'main'.
\--- org.springframework:spring-aop:5.3.4
     +--- org.springframework:spring-beans:5.3.4
     |    \--- org.springframework:spring-core:5.3.4
     |         \--- org.springframework:spring-jcl:5.3.4
     \--- org.springframework:spring-core:5.3.4 (*)

(*) - dependencies omitted (listed previously)

This shows us that spring-aop has 2 dependencies, which get added transitively to our project.

  1. spring-beans, which itself depends on spring-core, which itself depends on spring-jcl
  2. spring-core

Since spring-core is already contained in the first section of the tree as a dependency of spring-beans, when the dependency is repeated later on it’s marked with (*) to show that it was previously listed.

If Gradle weren’t to omit the dependencies, the tree would look like this instead:

compileClasspath - Compile classpath for source set 'main'.
\--- org.springframework:spring-aop:5.3.4
     +--- org.springframework:spring-beans:5.3.4
     |    \--- org.springframework:spring-core:5.3.4
     |         \--- org.springframework:spring-jcl:5.3.4
     \--- org.springframework:spring-core:5.3.4 (*)
          \--- org.springframework:spring-jcl:5.3.4

Thankfully though, Gradle keeps the dependency tree trimmed, making it much easier for our human brains to ingest. 🧠

10 Common Mistakes To Avoid In Your Gradle Project

Get your FREE PDF guide!

✅ Easy fixes you can apply today
✅ Build your project consistently every time
✅ Improve performance and maintainability

Failed dependencies

In the dependency tree if a dependency is marked as FAILED then Gradle wasn’t able to find it in any of the configured repositories.

Using simple example from above, we’ll make it even simpler by taking out the repositories declaration.

plugins {
    id 'java'
}

dependencies {
    implementation 'com.google.guava:guava:30.1-jre'
    implementation 'com.google.code.findbugs:jsr305:3.0.1'
}

Now when we generate the dependency tree we see the following:

$ ./gradlew dependencies --configuration compileClasspath

> Task :dependencies

------------------------------------------------------------
Root project
------------------------------------------------------------

compileClasspath - Compile classpath for source set 'main'.
+--- com.google.guava:guava:30.1-jre FAILED
\--- com.google.code.findbugs:jsr305:3.0.1 FAILED

A web-based, searchable dependency report is available by adding the --scan option.

This shows both dependencies with the FAILED state since they couldn’t be found without the correct repository definitions

Generating the dependency tree for multi-project builds

Unlike with most Gradle tasks, when you execute the dependencies task it only executes on a single project, and not any of it’s subporjects.

A project with a single subproject called my-subproject might be configured in the top-level build.gradle like this.

allprojects {
    apply plugin: 'java'
}

subprojects {
    repositories {
        mavenCentral()
    }

    dependencies {
        implementation 'com.google.guava:guava:30.1-jre'
    }
}

Or in other words, all projects are Java projects, but only the subprojects have guava defined as an implementation dependency.

If we run the dependencies task on the top level we’ll see an empty dependency tree:

$ ./gradlew dependencies --configuration compileClasspath

> Task :dependencies

------------------------------------------------------------
Root project
------------------------------------------------------------

compileClasspath - Compile classpath for source set 'main'.
No dependencies

Instead, we have to execute the task at the subproject level to see our dependency tree.

$ ./gradlew my-subproject:dependencies --configuration compileClasspath

> Task :my-subproject:dependencies

------------------------------------------------------------
Project :my-subproject
------------------------------------------------------------

compileClasspath - Compile classpath for source set 'main'.
\--- com.google.guava:guava:30.1-jre
     +--- com.google.guava:failureaccess:1.0.1
     +--- 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.5.0
     +--- com.google.errorprone:error_prone_annotations:2.3.4
     \--- com.google.j2objc:j2objc-annotations:1.3

Wrap up

You’ve seen how to use the dependencies task to print the Gradle dependency tree. The tree shows the dependencies for different dependency configurations, and includes details of how conflicts are resolved. This helps you understand how the various different classpaths are created in your project.

You might find these additional resources useful to continue your learning:

Get going with Gradle course
Gradle icon

Want to learn more about Gradle?
Check out the full selection of Gradle tutorials.

Gradle dependency tree

Leave a Reply

Your email address will not be published. Required fields are marked *

Scroll to top

Get the newsletter

Found this article helpful? Subscribe for monthly updates.

✅ All of my latest articles for the month
✅ Access to video tutorials
✅ Exclusive tips not found on my website