Gradle Dependency Resolution, Conflicts & Capabilities
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against Gradle 9.8.1 (the current release on 2026-10-16; 8.14.6 is the last 8.x release). Every example was run through the Gradle wrapper on
JDK 26, on macOS, in a scratch copy of the tidepool build: once with Kotlin DSL build files and once with Groovy DSL build files. The daemon and the compiler used the
same JDK 26 (toolchains are the subject of Toolchains). Unless a line says otherwise the console output
was identical in both DSLs, so each output is shown once. The builds ran with the configuration cache enabled (org.gradle.configuration-cache=true in
gradle.properties) and all of them were compatible with it, with one exception that is named where it happens: a build that fails while resolving dependencies cannot be
stored in the configuration cache, so the failure examples ran with --no-configuration-cache. Gradle 8.14 was not available here, so what is said about it comes from the
official documentation and release notes and is labelled "not run here". Console output was trimmed only in these ways: the header and the legends of the dependencies task, the > Task progress lines of the report tasks (dependencies, dependencyInsight and the like; the three listings that compile or run code keep them) and the line with the link to the
problems report were removed, the scratch directory was replaced by <work>, the attribute table of dependencyInsight was left out, and the generic hints at the end of a
failure message (--stacktrace, --info, --scan) were cut; where a listing is shortened in any other way the text says so. The decimal comma in the program output comes
from the locale of the machine.
The wrapper properties that a real project commits, with the official distribution URL (the scratch copies pointed the same file at a local copy of the same archive):
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionSha256Sum=dce76f55f8e251a3a1f130eb120f30b3d271de2b76c9b0729d316b5a1b6dc01f
distributionUrl=https\://services.gradle.org/distributions/gradle-9.8.1-bin.zip
networkTimeout=10000
retries=0
retryBackOffMs=500
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
Resolving dependencies is the part of a Gradle build that takes the most thought and goes wrong in the most surprising ways: a library that nobody declared is on the classpath, a version is not the one that was written in the build file, or two libraries that do the same job are both there. This page explains what Gradle does, step by step, and then shows the three tools that Gradle gives you for the three kinds of surprise: the rules for version conflicts, the capability mechanism for two modules that provide the same functionality, and exclusions for a dependency that should not be there at all. Declaring dependencies and the configurations they live in is on Declaring Dependencies & Configurations; rich versions, platforms and constraints in depth are on Platforms, Constraints & Rich Versions; forcing versions, substitution and metadata rules are on Resolution Rules & Substitution; variants and attributes are on Variant-Aware Resolution & Module Metadata. The vocabulary that all three build tools share (coordinates, transitive dependencies, scopes) is on Dependency Management Fundamentals, which also runs one version conflict through Ant with Ivy, Maven and Gradle; this page goes deeper on the Gradle half of that comparison.
The resolution model
Dependency resolution in Gradle is a two-step process. In the first step, graph resolution, Gradle builds the dependency graph from the declared dependencies and the metadata of the modules they point to. In the second step, artifact resolution, it turns the nodes of that graph into files and fetches them. The two steps are separate on purpose: many questions (which version? why is this here?) can be answered from the graph alone, without downloading a single JAR.
Graph resolution
The documentation names the building blocks in the order Gradle meets them.
| Term | Meaning |
|---|---|
Module |
A published unit such as |
Component |
One version of a module, such as |
Metadata |
The description of a component in the repository: a Gradle Module Metadata file ( |
Variant |
A variation of a component for one use, such as the API a library exposes to compilation or the runtime elements it needs when executed. Each variant lists its own dependencies and its own files. |
Attributes |
Key-value pairs ( |
Node and edge |
The graph is made of nodes, one per selected variant, and edges, one per dependency between two variants. |
Each resolvable configuration resolves to its own graph. That is why compileClasspath and runtimeClasspath of the same project can contain different versions and even
different modules: they ask for different attributes, and the metadata answers differently.
The loop that builds the graph works on one node at a time, starting from a queue that contains only the root node, which is the resolvable configuration itself. For every node it takes out of the queue, Gradle evaluates the node’s dependencies, decides which version each of them targets (this is where conflict resolution happens), downloads the metadata of all the components involved, selects a variant of each, and puts the selected variants back into the queue. The loop ends when the queue is empty. The documentation points out that this alternates between parallel metadata downloads and single-threaded logic.
Metadata comes from the repositories, and the rules for fetching it explain several surprises.
-
For a concrete version, Gradle checks the repositories in the order in which they are declared and stops at the first one that has metadata (a
.module, a.pomor anivy.xml) for it; a module that has only an artifact file ranks below one with metadata. All artifacts of the component are then taken from that same repository. -
For a dynamic version (
1.+,latest.release) Gradle must know which versions exist, so it asks all the repositories and does not stop at the first answer. For Maven repositories it readsmaven-metadata.xml; for Ivy repositories it lists a directory. -
A POM with a parent is resolved recursively, and everything, including misses, is stored in the dependency cache.
-
A repository that cannot be reached (unknown host, repeated connection failures, HTTP 5xx) is no longer checked for the rest of the build, after Gradle has retried with increasing wait times. Gradle does not move on to the next repository unless told to, so the resolution usually fails, which keeps builds reproducible: a build that silently skipped a faulty repository could give different results once it came back. The Repositories & Content Filtering page covers repository declaration and filtering, and Dependency Locking, Verification & Caching covers the cache.
-
mavenLocal()behaves like a cache of whatever Maven installed, so it can lack artifacts of a module whose metadata is there; the documentation warns about this in the same section.
Variant selection, the step that matches the attributes of the configuration against the variants in the metadata, has its own page: Variant-Aware Resolution & Module Metadata.
Two kinds of conflict are settled during graph resolution. A version conflict is two requests for different versions of the same module. A capability (or implementation) conflict is two different modules that declare the same capability. Both are the subject of this page.
Artifact resolution
Once the graph exists, Gradle maps each node to the files it needs. A variant can offer several artifact sets: the packaged JAR, the directory of compiled classes, the directory of resources, an archive of another type. For each node Gradle matches the requested attributes against the artifact sets it offers and picks one; if none matches, it tries to build a chain of artifact transforms that produces the requested form. How a project component hands out its classes directory instead of its JAR is the same mechanism.
The APIs that expose the result come in two groups. ResolutionResult gives the graph without downloading anything (components, variants, edges, selection reasons); the
files of a configuration, an ArtifactCollection or an ArtifactView trigger artifact selection and the download. The older ResolvedConfiguration and
LenientConfiguration APIs also trigger it and are in maintenance mode, so the documentation discourages them for new code.
Seeing the two phases
The tidepool build used in the rest of the page has three modules. The settings file declares the repository once, for all projects, and forbids project-level repositories:
rootProject.name = "tidepool"
dependencyResolutionManagement {
repositoriesMode = RepositoriesMode.FAIL_ON_PROJECT_REPOS
repositories {
mavenCentral()
}
}
include("tidepool-model", "tidepool-engine", "tidepool-cli")
rootProject.name = 'tidepool'
dependencyResolutionManagement {
repositoriesMode = RepositoriesMode.FAIL_ON_PROJECT_REPOS
repositories {
mavenCentral()
}
}
include 'tidepool-model', 'tidepool-engine', 'tidepool-cli'
The model is a plain library, the engine exposes the model through api and uses Commons CSV internally, and the command-line tool uses the engine, picocli and, as a project
often does for a library it also uses itself, commons-io 2.11.0:
plugins {
`java-library`
}
plugins {
id 'java-library'
}
plugins {
`java-library`
}
dependencies {
api(project(":tidepool-model"))
implementation("org.apache.commons:commons-csv:1.14.1")
}
plugins {
id 'java-library'
}
dependencies {
api project(':tidepool-model')
implementation 'org.apache.commons:commons-csv:1.14.1'
}
plugins {
application
}
dependencies {
implementation(project(":tidepool-engine"))
implementation("info.picocli:picocli:4.7.7")
implementation("commons-io:commons-io:2.11.0")
}
application {
mainClass = "io.tidepool.cli.Main"
}
plugins {
id 'application'
}
dependencies {
implementation project(':tidepool-engine')
implementation 'info.picocli:picocli:4.7.7'
implementation 'commons-io:commons-io:2.11.0'
}
application {
mainClass = 'io.tidepool.cli.Main'
}
The root gradle.properties holds the group and the version, which are the coordinates of the project components, and turns on the configuration cache:
org.gradle.configuration-cache=true
group=io.tidepool
version=1.0.0
The cli build file above adds one task for the next experiment. It receives the runtime classpath as an input file collection, which is what makes Gradle resolve the artifacts:
plugins {
application
}
dependencies {
implementation(project(":tidepool-engine"))
implementation("info.picocli:picocli:4.7.7")
implementation("commons-io:commons-io:2.11.0")
}
application {
mainClass = "io.tidepool.cli.Main"
}
// Asking for the files of a configuration is what triggers artifact resolution.
abstract class ShowFiles : DefaultTask() {
@get:InputFiles
abstract val files: ConfigurableFileCollection
@TaskAction
fun print() = files.forEach { println(it.name) }
}
tasks.register<ShowFiles>("showFiles") {
files.from(configurations.runtimeClasspath)
}
plugins {
id 'application'
}
dependencies {
implementation project(':tidepool-engine')
implementation 'info.picocli:picocli:4.7.7'
implementation 'commons-io:commons-io:2.11.0'
}
application {
mainClass = 'io.tidepool.cli.Main'
}
// Asking for the files of a configuration is what triggers artifact resolution.
abstract class ShowFiles extends DefaultTask {
@InputFiles
abstract ConfigurableFileCollection getFiles()
@TaskAction
void print() {
files.each { println it.name }
}
}
tasks.register('showFiles', ShowFiles) {
files.from(configurations.runtimeClasspath)
}
To watch the two phases, the experiment used a fresh Gradle user home so that its dependency cache started empty. First the graph only (dependencies resolves the graph and
prints it; it does not ask for a file), then the files. After each step the files in the cache (caches/modules-2/files-2.1 under the user home) were counted by type.
./gradlew -q :tidepool-cli:dependencies --configuration runtimeClasspath
runtimeClasspath - Runtime classpath of source set 'main'.
+--- project ':tidepool-engine'
| +--- project ':tidepool-model'
| \--- org.apache.commons:commons-csv:1.14.1
| +--- commons-io:commons-io:2.20.0
| \--- commons-codec:commons-codec:1.19.0
+--- info.picocli:picocli:4.7.7
\--- commons-io:commons-io:2.11.0 -> 2.20.0
The cache after that command held only metadata:
2 module
11 pom
Then the files were requested:
./gradlew :tidepool-cli:showFiles
> Task :tidepool-model:processResources NO-SOURCE
> Task :tidepool-engine:processResources NO-SOURCE
> Task :tidepool-model:compileJava UP-TO-DATE
> Task :tidepool-model:classes UP-TO-DATE
> Task :tidepool-model:jar UP-TO-DATE
> Task :tidepool-engine:compileJava UP-TO-DATE
> Task :tidepool-engine:classes UP-TO-DATE
> Task :tidepool-engine:jar UP-TO-DATE
> Task :tidepool-cli:showFiles
tidepool-engine-1.0.0.jar
picocli-4.7.7.jar
commons-csv-1.14.1.jar
commons-io-2.20.0.jar
tidepool-model-1.0.0.jar
commons-codec-1.19.0.jar
BUILD SUCCESSFUL in 951ms
5 actionable tasks: 1 executed, 4 up-to-date
and the cache was counted again:
4 jar
2 module
11 pom
Resolving the graph had fetched thirteen metadata files (eleven .pom and two .module, among them the parent POMs that the libraries inherit from and the BOM that they
import) and no JAR. Asking for the files fetched four
JARs: Commons CSV, Commons IO 2.20.0, Commons Codec and picocli. The two project components (tidepool-model and tidepool-engine) are built, not downloaded, and Commons IO
2.11.0, the version written in the cli build file, never became a file: Gradle read its POM during the graph phase, because someone asked for it, and then discarded it in
favour of 2.20.0. That is the whole story of this page in one listing, and the next sections explain why 2.20.0 won. The run was the same with the Groovy DSL build files.
Version conflicts
The highest version wins
When two paths of the graph request different versions of the same module, Gradle considers every requested version, wherever it appears in the graph, and by default selects
the highest. The tidepool graph has such a conflict: tidepool-cli declares commons-io 2.11.0, while Commons CSV 1.14.1, reached through tidepool-engine, requires 2.20.0.
The dependencies task prints the graph with the resolution already applied, and dependencyInsight explains one module:
./gradlew -q :tidepool-cli:dependencies --configuration runtimeClasspath
./gradlew -q :tidepool-cli:dependencyInsight --dependency commons-io --configuration runtimeClasspath
runtimeClasspath - Runtime classpath of source set 'main'.
+--- project ':tidepool-engine'
| +--- project ':tidepool-model'
| \--- org.apache.commons:commons-csv:1.14.1
| +--- commons-io:commons-io:2.20.0
| \--- commons-codec:commons-codec:1.19.0
+--- info.picocli:picocli:4.7.7
\--- commons-io:commons-io:2.11.0 -> 2.20.0
commons-io:commons-io:2.20.0
Variant runtime:
Selection reasons:
- By conflict resolution: between versions 2.20.0 and 2.11.0
commons-io:commons-io:2.20.0
\--- org.apache.commons:commons-csv:1.14.1
\--- project ':tidepool-engine'
\--- runtimeClasspath
commons-io:commons-io:2.11.0 -> 2.20.0
\--- runtimeClasspath
(The attribute table of the insight report, nine rows of org.gradle.* attributes with their provided and requested values, was left out.) These two reports are the
instrument for everything that follows, so they are worth reading closely.
-
commons-io:commons-io:2.11.0 -> 2.20.0means "requested on the left, selected on the right". The same module appears twice in the tree, once for each request, and the arrow is on the request that did not win. -
The first block of
dependencyInsightis the selected component, followed by its selection reasons.By conflict resolution: between versions 2.20.0 and 2.11.0is the answer to "why this version?". -
The following blocks are the requests, each shown with the chain of dependencies that led to it, up to the root configuration.
commons-io:commons-io:2.20.0comes fromcommons-csv, which comes from the projecttidepool-engine;commons-io:commons-io:2.11.0 -> 2.20.0comes straight fromruntimeClasspath, that is, from theclibuild file itself. -
In the tree,
(*)marks a subtree that was already expanded elsewhere,(c)a constraint (not a dependency), andFAILEDa node that could not be resolved. Thedependenciestask prints a legend for(*)and(c)under the tree; the legend of(*)is left out of the listings on this page, and the one of(c)is kept.
Source: Gradle User Manual: Resolving version conflicts, Gradle User Manual: understanding output annotations and identifying the reason for a dependency selection
"Highest" has consequences. The default rule compares versions and nothing else, so a direct declaration of an older version cannot downgrade a library that
needs a newer one: the 2.11.0 written in the cli build file is only a request, and it lost to a request that came from deeper in the graph. A newer version is also not always
compatible; when the winner is a major version that nobody tested with the library that asked for the older one, the build compiles and the problem appears at run time. The two
tools of the sections below, constraints and strictly, are the way to say what you want instead of accepting the default.
How versions are compared
The comparison is not alphabetical and not purely numeric. The versions of a module are split into parts at ., -, _ and +, and also where digits and letters meet
(1a1 is 1.a.1). The parts are compared one by one: numbers numerically, a number is higher than a word (1.a is lower than 1.1), words alphabetically and
case-sensitively, an extra numeric part makes a version higher (1.1 is lower than 1.1.0), and an extra word makes it lower (1.1.a is lower than 1.1). A few words are
special: dev is lower than any other word, and rc, snapshot, final, ga, release and sp are higher than any other word, in that order.
Conflict resolution adds one rule on top of the ordering: it compares base versions first. The part of a version after a non-dot separator is its qualifier (beta in
1.0-beta); Gradle selects the versions with the highest base version, discards all the others, and only then, between versions that share that base, prefers the ones
without a qualifier. Two experiments with slf4j-api show both halves. The experiment used a scratch pair of configurations, a dependency scope (where the versions are
declared) and a resolvable one that extends it, because the point was only to resolve and read the report.
plugins {
application
}
application {
mainClass = "io.tidepool.cli.Main"
}
// A scratch pair of configurations used only to compare version ordering.
val declared = configurations.dependencyScope("declared")
val ordering = configurations.resolvable("ordering") {
extendsFrom(declared.get())
}
dependencies {
declared("org.slf4j:slf4j-api:2.0.0-beta1")
declared("org.slf4j:slf4j-api:2.0.0")
declared("org.slf4j:slf4j-api:2.0.0-alpha7")
}
plugins {
id 'application'
}
application {
mainClass = 'io.tidepool.cli.Main'
}
// A scratch pair of configurations used only to compare version ordering.
configurations.dependencyScope('declared')
configurations.resolvable('ordering') {
extendsFrom(configurations.getByName('declared'))
}
dependencies {
declared 'org.slf4j:slf4j-api:2.0.0-beta1'
declared 'org.slf4j:slf4j-api:2.0.0'
declared 'org.slf4j:slf4j-api:2.0.0-alpha7'
}
./gradlew -q :tidepool-cli:dependencyInsight --dependency slf4j-api --configuration ordering
org.slf4j:slf4j-api:2.0.0
Variant runtime:
Selection reasons:
- By conflict resolution: between versions 2.0.0, 2.0.0-beta1 and 2.0.0-alpha7
org.slf4j:slf4j-api:2.0.0
\--- ordering
org.slf4j:slf4j-api:2.0.0-alpha7 -> 2.0.0
\--- ordering
org.slf4j:slf4j-api:2.0.0-beta1 -> 2.0.0
\--- ordering
The release 2.0.0 beat its own beta and alpha: same base version, and the one without a qualifier is preferred. In the second experiment the declarations were 2.0.17 and
2.1.0-alpha1:
org.slf4j:slf4j-api:2.1.0-alpha1
Variant runtime:
Selection reasons:
- By conflict resolution: between versions 2.1.0-alpha1 and 2.0.17
org.slf4j:slf4j-api:2.1.0-alpha1
\--- ordering
org.slf4j:slf4j-api:2.0.17 -> 2.1.0-alpha1
\--- ordering
The alpha won, because its base version 2.1.0 is higher than 2.0.17. Gradle does not know that 2.1.0-alpha1 is a pre-release in any sense that matters for your project, so a
transitive dependency on a pre-release can pull one into the build. Rejecting it is what reject and strictly ranges are for (see
Platforms, Constraints & Rich Versions).
Ranges take part in the same decision. Without ranges, the highest non-rejected version is selected. When a non-range version fits within a range, or is higher than its upper bound, it is selected. When there are only ranges, the highest version in their intersection is selected if they overlap; otherwise the highest version of the largest range, or a failure if there is no such version. Resolving a range needs the list of versions, which Gradle reads from the repository (see the metadata rules above).
A constraint is a request, not an override
A dependency constraint is a statement about a version that applies only if the module is part of the graph for some other reason. It is the supported way to influence the
version of a transitive dependency without making it a direct one. The cli build file below constrains commons-io to 2.21.0:
plugins {
application
}
dependencies {
implementation(project(":tidepool-engine"))
implementation("info.picocli:picocli:4.7.7")
implementation("commons-io:commons-io:2.11.0")
constraints {
implementation("commons-io:commons-io:2.21.0") {
because("tidepool-cli standardises on commons-io 2.21.0")
}
}
}
application {
mainClass = "io.tidepool.cli.Main"
}
plugins {
id 'application'
}
dependencies {
implementation project(':tidepool-engine')
implementation 'info.picocli:picocli:4.7.7'
implementation 'commons-io:commons-io:2.11.0'
constraints {
implementation('commons-io:commons-io:2.21.0') {
because 'tidepool-cli standardises on commons-io 2.21.0'
}
}
}
application {
mainClass = 'io.tidepool.cli.Main'
}
./gradlew -q :tidepool-cli:dependencies --configuration runtimeClasspath
./gradlew -q :tidepool-cli:dependencyInsight --dependency commons-io --configuration runtimeClasspath
runtimeClasspath - Runtime classpath of source set 'main'.
+--- project ':tidepool-engine'
| +--- project ':tidepool-model'
| \--- org.apache.commons:commons-csv:1.14.1
| +--- commons-io:commons-io:2.20.0 -> 2.21.0
| \--- commons-codec:commons-codec:1.19.0
+--- info.picocli:picocli:4.7.7
+--- commons-io:commons-io:2.11.0 -> 2.21.0
\--- commons-io:commons-io:2.21.0 (c)
(c) - A dependency constraint, not a dependency. The dependency affected by the constraint occurs elsewhere in the tree.
commons-io:commons-io:2.21.0
Variant runtime:
Selection reasons:
- By constraint: tidepool-cli standardises on commons-io 2.21.0
- By conflict resolution: between versions 2.21.0, 2.11.0 and 2.20.0
commons-io:commons-io:2.21.0
\--- runtimeClasspath
commons-io:commons-io:2.11.0 -> 2.21.0
\--- runtimeClasspath
commons-io:commons-io:2.20.0 -> 2.21.0
\--- org.apache.commons:commons-csv:1.14.1
\--- project ':tidepool-engine'
\--- runtimeClasspath
Both requests (2.11.0 from the build file, 2.20.0 from Commons CSV) now resolve to 2.21.0, and the selection reasons list the constraint with its because text, followed by
the conflict resolution between all three versions. The constraint participated in the conflict like any other request, and it won because it is the highest. A constraint
that is lower than the requests does not lower anything. With 2.15.0 instead of 2.21.0:
runtimeClasspath - Runtime classpath of source set 'main'.
+--- project ':tidepool-engine'
| +--- project ':tidepool-model'
| \--- org.apache.commons:commons-csv:1.14.1
| +--- commons-io:commons-io:2.20.0
| \--- commons-codec:commons-codec:1.19.0
+--- info.picocli:picocli:4.7.7
+--- commons-io:commons-io:2.11.0 -> 2.20.0
\--- commons-io:commons-io:2.15.0 -> 2.20.0 (c)
(c) - A dependency constraint, not a dependency. The dependency affected by the constraint occurs elsewhere in the tree.
the selected version is still 2.20.0, and the constraint ((c)) is shown as 2.15.0 -> 2.20.0. The consequence is a rule worth remembering: constraints can raise the
version of a module and can pin the one a platform recommends, but under the default rules they can never lower it. Lowering takes strictly, which is the next section;
platforms, BOMs and constraints published by libraries are the subject of
Platforms, Constraints & Rich Versions.
strictly: saying exactly which version
A strict version is the strongest declaration. Every version that does not match it is excluded, and, as the documentation puts it, when it is used on a declared dependency
it can downgrade a version. The cli build file below makes 2.11.0 the only acceptable commons-io:
plugins {
application
}
dependencies {
implementation(project(":tidepool-engine"))
implementation("info.picocli:picocli:4.7.7")
implementation("commons-io:commons-io") {
version {
strictly("2.11.0")
}
}
}
application {
mainClass = "io.tidepool.cli.Main"
}
plugins {
id 'application'
}
dependencies {
implementation project(':tidepool-engine')
implementation 'info.picocli:picocli:4.7.7'
implementation('commons-io:commons-io') {
version {
strictly '2.11.0'
}
}
}
application {
mainClass = 'io.tidepool.cli.Main'
}
runtimeClasspath - Runtime classpath of source set 'main'.
+--- project ':tidepool-engine'
| +--- project ':tidepool-model'
| \--- org.apache.commons:commons-csv:1.14.1
| +--- commons-io:commons-io:2.20.0 -> 2.11.0
| \--- commons-codec:commons-codec:1.19.0
+--- info.picocli:picocli:4.7.7
\--- commons-io:commons-io:{strictly 2.11.0} -> 2.11.0
commons-io:commons-io:2.11.0 (by ancestor)
Variant runtime:
commons-io:commons-io:{strictly 2.11.0} -> 2.11.0
\--- runtimeClasspath
commons-io:commons-io:2.20.0 -> 2.11.0
\--- org.apache.commons:commons-csv:1.14.1
\--- project ':tidepool-engine'
\--- runtimeClasspath
Commons CSV asks for 2.20.0 and gets 2.11.0. The selection report says (by ancestor): the strict version of a node closer to the root overrides what the nodes below it
request. There is no conflict resolution in the reasons any more, because there was nothing to
choose between. A downgrade like this one is a deliberate decision to run Commons CSV with a Commons IO older than the one it was built against; do it only when a test shows
that it works.
When strict declarations at the same level disagree there is no right answer, and Gradle says so. The cli build file below declares a strict 2.11.0 on the dependency and a
strict 2.12.0 in a constraints block:
plugins {
application
}
dependencies {
implementation(project(":tidepool-engine"))
implementation("info.picocli:picocli:4.7.7")
implementation("commons-io:commons-io") {
version {
strictly("2.11.0")
}
}
constraints {
implementation("commons-io:commons-io") {
version {
strictly("2.12.0")
}
}
}
}
application {
mainClass = "io.tidepool.cli.Main"
}
plugins {
id 'application'
}
dependencies {
implementation project(':tidepool-engine')
implementation 'info.picocli:picocli:4.7.7'
implementation('commons-io:commons-io') {
version {
strictly '2.11.0'
}
}
constraints {
implementation('commons-io:commons-io') {
version {
strictly '2.12.0'
}
}
}
}
application {
mainClass = 'io.tidepool.cli.Main'
}
./gradlew -q :tidepool-cli:dependencies --configuration runtimeClasspath
./gradlew --no-configuration-cache :tidepool-cli:compileJava
runtimeClasspath - Runtime classpath of source set 'main'.
+--- project ':tidepool-engine'
| +--- project ':tidepool-model'
| \--- org.apache.commons:commons-csv:1.14.1
| +--- commons-io:commons-io:2.20.0 FAILED
| \--- commons-codec:commons-codec:1.19.0
+--- info.picocli:picocli:4.7.7
+--- commons-io:commons-io:{strictly 2.11.0} FAILED
\--- commons-io:commons-io:{strictly 2.12.0} FAILED
FAILURE: Build failed with an exception.
* What went wrong:
Execution failed for task ':tidepool-cli:compileJava' (registered by plugin class 'org.gradle.api.plugins.JavaBasePlugin').
> Could not resolve all files for configuration ':tidepool-cli:compileClasspath'.
> Could not resolve commons-io:commons-io:{strictly 2.11.0}.
Required by:
project ':tidepool-cli'
> Cannot find a version of 'commons-io:commons-io' that satisfies the version constraints:
Dependency path: project ':tidepool-cli' (compileClasspath) --> 'commons-io:commons-io:{strictly 2.11.0}'
Constraint path: project ':tidepool-cli' (compileClasspath) --> 'commons-io:commons-io:{strictly 2.12.0}'
> There is 1 more failure with an identical cause.
The dependencies task reports the problem but does not fail (the nodes are marked FAILED); the task that needs the classpath does, with both the dependency path and the
constraint path in the message. The full rules for competing strict declarations at different depths, and for ranges and prefer, are in the documentation and on the
platforms page.
Failing instead of choosing: failOnVersionConflict
Some teams prefer to be told about every disagreement instead of accepting the highest version. ResolutionStrategy.failOnVersionConflict() makes any version conflict in a
configuration a failure. It is configured per configuration, so configureEach applies it to all of them:
plugins {
application
}
configurations.configureEach {
resolutionStrategy.failOnVersionConflict()
}
dependencies {
implementation(project(":tidepool-engine"))
implementation("info.picocli:picocli:4.7.7")
implementation("commons-io:commons-io:2.11.0")
}
application {
mainClass = "io.tidepool.cli.Main"
}
plugins {
id 'application'
}
configurations.configureEach {
resolutionStrategy.failOnVersionConflict()
}
dependencies {
implementation project(':tidepool-engine')
implementation 'info.picocli:picocli:4.7.7'
implementation 'commons-io:commons-io:2.11.0'
}
application {
mainClass = 'io.tidepool.cli.Main'
}
./gradlew --no-configuration-cache :tidepool-cli:compileJava
./gradlew --no-configuration-cache :tidepool-cli:installDist
> Task :tidepool-model:compileJava UP-TO-DATE
> Task :tidepool-engine:compileJava UP-TO-DATE
> Task :tidepool-cli:compileJava UP-TO-DATE
BUILD SUCCESSFUL in 426ms
3 actionable tasks: 3 up-to-date
The first command succeeded. The compile classpath of tidepool-cli has only one request for commons-io, the 2.11.0 that the build file declares: the engine declares
Commons CSV with implementation, so its dependencies are not exposed to the compile classpath of a consumer, and there was nothing to conflict. The second command resolves
runtimeClasspath, where the engine’s Commons CSV is present, and where the conflict is:
FAILURE: Build failed with an exception.
* What went wrong:
Execution failed for task ':tidepool-cli:startScripts' (registered by plugin 'org.gradle.application').
> Error while evaluating property 'relativeClasspath' of task ':tidepool-cli:startScripts'.
> Could not resolve all files for configuration ':tidepool-cli:runtimeClasspath'.
> Could not resolve commons-io:commons-io:2.11.0.
Required by:
project ':tidepool-cli'
> Conflict found for module 'commons-io:commons-io': between versions 2.20.0 and 2.11.0
> There is 1 more failure with an identical cause.
The message names the module and the two versions, and the hint at the end (cut here) suggests the dependencyInsight command that finds the origin of each request. The
setting is blunt: a graph of any size has benign version differences, and failing on all of them tends to be turned off again. A common compromise is to enable it only in a
CI job or in a verification task, and to resolve the conflicts it reports with constraints or strictly.
The Javadoc of ResolutionStrategy lists the neighbouring switches: failOnDynamicVersions(), failOnChangingVersions() and failOnNonReproducibleResolution(), which belong
to dependency locking and are covered on the locking page.
The other ways out
The default rule, the constraint and strictly are the tools of the dependency graph itself. Gradle has more, and each lives on the page named in the last column.
| Need | Tool | Where |
|---|---|---|
Raise the version of a transitive dependency |
A constraint on the module, or a platform that recommends it |
This page; Platforms, Constraints & Rich Versions |
Run exactly one version, whatever the graph says |
|
This page |
Keep several libraries on one version |
A platform or BOM, or version alignment |
|
Force a version for a whole configuration, or replace one module by another or by a project |
|
|
Fix wrong metadata (a missing or unnecessary dependency in someone’s POM) |
A component metadata rule |
|
Freeze the whole result and detect change |
Dependency locking |
|
Remove a dependency that should not be there |
|
The section "Excluding transitive dependencies" below |
The same graph in Maven
Maven resolves the same graph with a different rule: nearest definition. The version closest to the root of the graph wins, and the first declaration wins at equal depth.
The Maven 3.10.0 equivalent of the cli build is a POM with the same two dependencies:
<dependencies>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-csv</artifactId>
<version>1.14.1</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.11.0</version>
</dependency>
</dependencies>
mvn -B org.apache.maven.plugins:maven-dependency-plugin:3.11.0:tree -Dverbose
[INFO] io.tidepool:tidepool-cli:jar:1.0.0
[INFO] +- org.apache.commons:commons-csv:jar:1.14.1:compile
[INFO] | +- (commons-io:commons-io:jar:2.20.0:compile - omitted for conflict with 2.11.0)
[INFO] | \- commons-codec:commons-codec:jar:1.19.0:compile
[INFO] \- commons-io:commons-io:jar:2.11.0:compile
Maven kept 2.11.0 because the declaration in the POM is one level from the root while the one in Commons CSV is two. Gradle kept 2.20.0 because it is higher. The two rules
agree whenever the direct declaration is also the newest; they disagree exactly when a project pins an older version of something a library needs. Maven’s remedies are a
dependencyManagement entry (which does not override a version written in a direct dependency), exclusions, and the Enforcer rules dependencyConvergence and
requireUpperBoundDeps, which fail the build instead of choosing; they are on
Maven Dependencies, Scopes & Mediation,
Dependency Management & BOMs and
Dependency Analysis & Convergence. In Ant, Ivy picks a conflict manager per
module; its default, latest-revision, behaves like Gradle’s rule and nearest (since Ivy 2.6.0) behaves like Maven’s, as
Ivy: Conflicts & Versions shows. The Maven output above has the line
Loaded 74 auto-discovered prefixes for remote repository ... removed.
Capability conflicts
What a capability is
Version conflicts are about one module at two versions. A different failure is two different modules that do the same job: two logging bindings, two connection pools, a library and its relocated successor. Their coordinates differ, so a tool that looks only at coordinates (Maven, or Gradle’s version conflict logic) sees no conflict, and both end up on the classpath. Gradle’s answer is the capability: a name, written as group, name and version, that a component declares in order to say "I provide this functionality". It is illegal for a dependency graph to contain two components that provide the same capability; Gradle fails the build and names the modules.
Every component has an implicit capability, its own coordinates: org.apache.commons:commons-lang3:3.8 provides the capability org.apache.commons:commons-lang3, at
version 3.8. Capabilities are versioned, which is what lets Gradle choose the highest one. A component can declare more capabilities, and doing so is the whole mechanism. Two
places can declare them: the metadata of a published component (Gradle Module Metadata has a capabilities section for each variant), and a build, either through a
component metadata rule for an external module that did not declare them or through the outgoing configurations of your own project.
A conflict you get for free: Guava and google-collections
Guava is the successor of the Google Collections Library. Guava’s Gradle Module Metadata declares, on each of its variants, the capability
com.google.collections:google-collections next to its own, so a build that has both Guava and the old library fails without any rule being written. The metadata of
com.google.guava:guava:33.5.0-jre (the file guava-33.5.0-jre.module, fetched from Maven Central) has this in each variant:
"capabilities": [
{
"group": "com.google.guava",
"name": "guava",
"version": "33.5.0-jre"
},
{
"group": "com.google.collections",
"name": "google-collections",
"version": "33.5.0-jre"
}
]
(The listing is the capabilities entry of the first variant of the file; the three other variants repeat it. Note that the capability version of google-collections is the
version of Guava, which is what makes Guava the highest provider.) The cli build file declares both libraries:
plugins {
application
}
dependencies {
implementation(project(":tidepool-engine"))
implementation("info.picocli:picocli:4.7.7")
implementation("com.google.guava:guava:33.5.0-jre")
implementation("com.google.collections:google-collections:1.0")
}
application {
mainClass = "io.tidepool.cli.Main"
}
plugins {
id 'application'
}
dependencies {
implementation project(':tidepool-engine')
implementation 'info.picocli:picocli:4.7.7'
implementation 'com.google.guava:guava:33.5.0-jre'
implementation 'com.google.collections:google-collections:1.0'
}
application {
mainClass = 'io.tidepool.cli.Main'
}
./gradlew --no-configuration-cache :tidepool-cli:compileJava
FAILURE: Build failed with an exception.
* What went wrong:
Execution failed for task ':tidepool-cli:compileJava' (registered by plugin class 'org.gradle.api.plugins.JavaBasePlugin').
> Could not resolve all files for configuration ':tidepool-cli:compileClasspath'.
> Could not resolve com.google.guava:guava:33.5.0-jre.
Required by:
project ':tidepool-cli'
> Module 'com.google.guava:guava' has been rejected:
Cannot select module because of conflict with com.google.collections:google-collections:1.0 (compile). Both provide capability 'com.google.collections:google-collections:33.5.0-jre'.
> Could not resolve com.google.collections:google-collections:1.0.
Required by:
project ':tidepool-cli'
> Module 'com.google.collections:google-collections' has been rejected:
Cannot select module because of conflict with com.google.guava:guava:33.5.0-jre (jreApiElements). Both provide capability 'com.google.collections:google-collections:1.0'.
* Try:
> Capability conflicts are explained in more detail at https://docs.gradle.org/9.8.1/userguide/component_capabilities.html#sub:capabilities.
> Use 'resolutionStrategy.capabilitiesResolution' to choose between conflicting capability providers, as described at https://docs.gradle.org/9.8.1/userguide/component_capabilities.html#sec:selecting-between-candidates.
Read the message once: it names both modules, the capability on which they collide (com.google.collections:google-collections), and the variant that provides it (compile
for the old library, which has only POM metadata, and jreApiElements for Guava). It also prints two hints. The first one links the section of the documentation with the
anchor sub:capabilities, which is not an anchor of that page (its section is sec:capabilities), so the link lands at the top of the page; the second hint is the useful one,
resolutionStrategy.capabilitiesResolution. The reports show the same: the dependencies task marks both nodes FAILED.
Choosing between candidates
resolutionStrategy.capabilitiesResolution.withCapability(...) receives an action that is run whenever a conflict on that capability appears. The action has the list of
candidates, and can call select(candidate) for one of them, or selectHighestVersion(), which compares the capability versions (here Guava’s 33.5.0-jre against the old
library’s 1.0), and may call because(...) to leave a reason that appears in the reports.
plugins {
application
}
configurations.configureEach {
resolutionStrategy.capabilitiesResolution.withCapability("com.google.collections:google-collections") {
selectHighestVersion()
}
}
dependencies {
implementation(project(":tidepool-engine"))
implementation("info.picocli:picocli:4.7.7")
implementation("com.google.guava:guava:33.5.0-jre")
implementation("com.google.collections:google-collections:1.0")
}
application {
mainClass = "io.tidepool.cli.Main"
}
plugins {
id 'application'
}
configurations.configureEach {
resolutionStrategy.capabilitiesResolution.withCapability('com.google.collections:google-collections') {
selectHighestVersion()
}
}
dependencies {
implementation project(':tidepool-engine')
implementation 'info.picocli:picocli:4.7.7'
implementation 'com.google.guava:guava:33.5.0-jre'
implementation 'com.google.collections:google-collections:1.0'
}
application {
mainClass = 'io.tidepool.cli.Main'
}
./gradlew -q :tidepool-cli:dependencies --configuration runtimeClasspath
./gradlew -q :tidepool-cli:dependencyInsight --dependency guava:guava --configuration runtimeClasspath
runtimeClasspath - Runtime classpath of source set 'main'.
+--- project ':tidepool-engine'
| +--- project ':tidepool-model'
| \--- org.apache.commons:commons-csv:1.14.1
| +--- commons-io:commons-io:2.20.0
| \--- commons-codec:commons-codec:1.19.0
+--- info.picocli:picocli:4.7.7
+--- com.google.guava:guava:33.5.0-jre
| +--- com.google.guava:failureaccess:1.0.3
| +--- com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava
| +--- org.jspecify:jspecify:1.0.0
| +--- com.google.errorprone:error_prone_annotations:2.41.0
| \--- com.google.j2objc:j2objc-annotations:3.1
\--- com.google.collections:google-collections:1.0 -> com.google.guava:guava:33.5.0-jre (*)
com.google.guava:guava:33.5.0-jre
Variant jreRuntimeElements:
Selection reasons:
- By conflict resolution: latest version of capability com.google.collections:google-collections
com.google.collections:google-collections:1.0 -> com.google.guava:guava:33.5.0-jre
\--- runtimeClasspath
com.google.guava:guava:33.5.0-jre
\--- runtimeClasspath
Guava was selected, and the request for google-collections was replaced by Guava: the arrow google-collections:1.0 -> com.google.guava:guava:33.5.0-jre is a request
for one module that resolved to another, which is how a build ends up with Guava’s classes in place of the old ones. The reason is latest version of capability. When the
highest version is the wrong criterion (for logging frameworks any version of the right binding will do), choose the candidate by identity and say why:
plugins {
application
}
configurations.configureEach {
resolutionStrategy.capabilitiesResolution.withCapability("com.google.collections:google-collections") {
val guava = candidates.first {
val id = it.id
id is ModuleComponentIdentifier && id.module == "guava"
}
select(guava)
because("Guava is the successor of google-collections")
}
}
dependencies {
implementation(project(":tidepool-engine"))
implementation("info.picocli:picocli:4.7.7")
implementation("com.google.guava:guava:33.5.0-jre")
implementation("com.google.collections:google-collections:1.0")
}
application {
mainClass = "io.tidepool.cli.Main"
}
plugins {
id 'application'
}
configurations.configureEach {
resolutionStrategy.capabilitiesResolution.withCapability('com.google.collections:google-collections') {
def guava = candidates.find {
it.id instanceof ModuleComponentIdentifier && it.id.module == 'guava'
}
select(guava)
because 'Guava is the successor of google-collections'
}
}
dependencies {
implementation project(':tidepool-engine')
implementation 'info.picocli:picocli:4.7.7'
implementation 'com.google.guava:guava:33.5.0-jre'
implementation 'com.google.collections:google-collections:1.0'
}
application {
mainClass = 'io.tidepool.cli.Main'
}
com.google.guava:guava:33.5.0-jre
Variant jreRuntimeElements:
Selection reasons:
- By conflict resolution: On capability com.google.collections:google-collections Guava is the successor of google-collections
com.google.collections:google-collections:1.0 -> com.google.guava:guava:33.5.0-jre
\--- runtimeClasspath
com.google.guava:guava:33.5.0-jre
\--- runtimeClasspath
The reason now carries the text of because. The build compiled (BUILD SUCCESSFUL) and the configuration cache stored the entry, which means that this resolution rule is
configuration-cache compatible. The documentation lists the limits of select: it accepts only a module that is currently a candidate in that conflict, select(null) is an
error, and if the module you prefer takes part in another conflict the one you are resolving stays unresolved and the build fails.
Maven has no capabilities. The Maven 3.10.0 resolution of the same two dependencies keeps both, and the duplicate classes are discovered at run time:
[INFO] io.tidepool:tidepool-cli:jar:1.0.0
[INFO] +- com.google.guava:guava:jar:33.5.0-jre:compile
[INFO] | +- com.google.guava:failureaccess:jar:1.0.3:compile
[INFO] | +- com.google.guava:listenablefuture:jar:9999.0-empty-to-avoid-conflict-with-guava:compile
[INFO] | +- org.jspecify:jspecify:jar:1.0.0:compile
[INFO] | +- com.google.errorprone:error_prone_annotations:jar:2.41.0:compile
[INFO] | \- com.google.j2objc:j2objc-annotations:jar:3.1:compile
[INFO] \- com.google.collections:google-collections:jar:1.0:compile
The listenablefuture entry in the middle of the tree shows a version-based trick for a related problem: Guava depends on a listenablefuture whose version is
9999.0-empty-to-avoid-conflict-with-guava, a version number that, by its name, is meant to win every version comparison against the standalone ListenableFuture library. It
works by version conflict resolution; the capability on google-collections is the mechanism that works across different coordinates.
Declaring capabilities for an external module
Most libraries do not declare capabilities, so the conflict must be declared by the build. The two modules of the documentation’s example are log4j:log4j (Log4j 1.x) and
org.slf4j:log4j-over-slf4j (a bridge that supplies the Log4j 1 API on top of SLF4J): different coordinates, same API, and both are fatal on one classpath. A
ComponentMetadataRule can add a capability to every variant of both. The components.all call registers the rule. The rule is a class and not a closure, which is what keeps the build compatible with the configuration cache (the build ran with
it, and stored the entry); a rule can also be marked @CacheableRule so that Gradle caches its result, as described on
Resolution Rules & Substitution.
plugins {
application
}
// Metadata rule: two unrelated coordinates provide the same capability.
class Log4jApiCapability : ComponentMetadataRule {
override fun execute(context: ComponentMetadataContext) = context.details.run {
if (id.name == "log4j" || id.name == "log4j-over-slf4j") {
allVariants {
withCapabilities {
addCapability("io.tidepool", "log4j-api", id.version)
}
}
}
}
}
dependencies {
components.all<Log4jApiCapability>()
implementation(project(":tidepool-engine"))
implementation("info.picocli:picocli:4.7.7")
implementation("log4j:log4j:1.2.17")
implementation("org.slf4j:log4j-over-slf4j:2.0.17")
}
application {
mainClass = "io.tidepool.cli.Main"
}
import groovy.transform.CompileStatic
plugins {
id 'application'
}
// Metadata rule: two unrelated coordinates provide the same capability.
@CompileStatic
class Log4jApiCapability implements ComponentMetadataRule {
void execute(ComponentMetadataContext context) {
context.details.with {
if (id.name == 'log4j' || id.name == 'log4j-over-slf4j') {
allVariants {
it.withCapabilities {
it.addCapability('io.tidepool', 'log4j-api', id.version)
}
}
}
}
}
}
dependencies {
components.all(Log4jApiCapability)
implementation project(':tidepool-engine')
implementation 'info.picocli:picocli:4.7.7'
implementation 'log4j:log4j:1.2.17'
implementation 'org.slf4j:log4j-over-slf4j:2.0.17'
}
application {
mainClass = 'io.tidepool.cli.Main'
}
./gradlew --no-configuration-cache :tidepool-cli:compileJava
FAILURE: Build failed with an exception.
* What went wrong:
Execution failed for task ':tidepool-cli:compileJava' (registered by plugin class 'org.gradle.api.plugins.JavaBasePlugin').
> Could not resolve all files for configuration ':tidepool-cli:compileClasspath'.
> Could not resolve log4j:log4j:1.2.17.
Required by:
project ':tidepool-cli'
> Module 'log4j:log4j' has been rejected:
Cannot select module because of conflict with org.slf4j:log4j-over-slf4j:2.0.17 (compile). Both provide capability 'io.tidepool:log4j-api:1.2.17'.
> Could not resolve org.slf4j:log4j-over-slf4j:2.0.17.
Required by:
project ':tidepool-cli'
> Module 'org.slf4j:log4j-over-slf4j' has been rejected:
Cannot select module because of conflict with log4j:log4j:1.2.17 (compile). Both provide capability 'io.tidepool:log4j-api:2.0.17'.
The documentation’s version of this rule gives both modules the capability log4j:log4j, the coordinates of the old library itself. The example above uses a neutral name,
io.tidepool:log4j-api, for a reason found while testing: with log4j:log4j as the capability and the two libraries both declared directly, Gradle 9.8.1 found no conflict
when the old library was declared first, and found it when the bridge was declared first. The same two declarations with addCapability("log4j", "log4j", id.version):
compileClasspath - Compile classpath for source set 'main'.
+--- project ':tidepool-engine'
| \--- project ':tidepool-model'
+--- info.picocli:picocli:4.7.7
+--- log4j:log4j:1.2.17
\--- org.slf4j:log4j-over-slf4j:2.0.17
\--- org.slf4j:slf4j-api:2.0.17
(log4j:log4j declared first: no conflict, both libraries on the compile classpath.) With the two lines swapped:
compileClasspath - Compile classpath for source set 'main'.
+--- project ':tidepool-engine'
| \--- project ':tidepool-model'
+--- info.picocli:picocli:4.7.7
+--- org.slf4j:log4j-over-slf4j:2.0.17 FAILED
\--- log4j:log4j:1.2.17 FAILED
(the bridge declared first: both nodes FAILED, and compileJava fails with the same capability message as above). The Groovy DSL build files behaved the same in both orders,
and the documentation’s own variant of the example, with log4j:log4j arriving transitively through ZooKeeper 3.4.9, did fail as the documentation says. The cause was not
investigated; the practical rule is to name the capability after neither module, and to check the dependencies report for FAILED instead of assuming that a missing
conflict means a missing problem.
Once the capability exists the conflict is resolved as before, with a candidate chosen by identity (a bridge is not an upgrade of the library it replaces, so
selectHighestVersion() would be wrong):
plugins {
application
}
// Metadata rule: two unrelated coordinates provide the same capability.
class Log4jApiCapability : ComponentMetadataRule {
override fun execute(context: ComponentMetadataContext) = context.details.run {
if (id.name == "log4j" || id.name == "log4j-over-slf4j") {
allVariants {
withCapabilities {
addCapability("io.tidepool", "log4j-api", id.version)
}
}
}
}
}
configurations.configureEach {
resolutionStrategy.capabilitiesResolution.withCapability("io.tidepool:log4j-api") {
val bridge = candidates.first {
val id = it.id
id is ModuleComponentIdentifier && id.module == "log4j-over-slf4j"
}
select(bridge)
because("tidepool logs through SLF4J")
}
}
dependencies {
components.all<Log4jApiCapability>()
implementation(project(":tidepool-engine"))
implementation("info.picocli:picocli:4.7.7")
implementation("log4j:log4j:1.2.17")
implementation("org.slf4j:log4j-over-slf4j:2.0.17")
}
application {
mainClass = "io.tidepool.cli.Main"
}
import groovy.transform.CompileStatic
plugins {
id 'application'
}
// Metadata rule: two unrelated coordinates provide the same capability.
@CompileStatic
class Log4jApiCapability implements ComponentMetadataRule {
void execute(ComponentMetadataContext context) {
context.details.with {
if (id.name == 'log4j' || id.name == 'log4j-over-slf4j') {
allVariants {
it.withCapabilities {
it.addCapability('io.tidepool', 'log4j-api', id.version)
}
}
}
}
}
}
configurations.configureEach {
resolutionStrategy.capabilitiesResolution.withCapability('io.tidepool:log4j-api') {
def bridge = candidates.find {
it.id instanceof ModuleComponentIdentifier && it.id.module == 'log4j-over-slf4j'
}
select(bridge)
because 'tidepool logs through SLF4J'
}
}
dependencies {
components.all(Log4jApiCapability)
implementation project(':tidepool-engine')
implementation 'info.picocli:picocli:4.7.7'
implementation 'log4j:log4j:1.2.17'
implementation 'org.slf4j:log4j-over-slf4j:2.0.17'
}
application {
mainClass = 'io.tidepool.cli.Main'
}
./gradlew -q :tidepool-cli:dependencies --configuration runtimeClasspath
./gradlew -q :tidepool-cli:dependencyInsight --dependency log4j --configuration runtimeClasspath
runtimeClasspath - Runtime classpath of source set 'main'.
+--- project ':tidepool-engine'
| +--- project ':tidepool-model'
| \--- org.apache.commons:commons-csv:1.14.1
| +--- commons-io:commons-io:2.20.0
| \--- commons-codec:commons-codec:1.19.0
+--- info.picocli:picocli:4.7.7
+--- log4j:log4j:1.2.17 -> org.slf4j:log4j-over-slf4j:2.0.17
| \--- org.slf4j:slf4j-api:2.0.17
\--- org.slf4j:log4j-over-slf4j:2.0.17 (*)
org.slf4j:log4j-over-slf4j:2.0.17
Variant runtime:
Selection reasons:
- By conflict resolution: On capability io.tidepool:log4j-api tidepool logs through SLF4J
log4j:log4j:1.2.17 -> org.slf4j:log4j-over-slf4j:2.0.17
\--- runtimeClasspath
org.slf4j:log4j-over-slf4j:2.0.17
\--- runtimeClasspath
Log4j 1 is gone and the bridge, with its slf4j-api, took its place. Both modules publish POM metadata only (the bridge has no .module file on Maven Central), which is why a
rule is needed at all. The documentation recommends packaging rules like this one in a plugin, so that every project of an organisation shares them (see
Convention Plugins & Sharing Build Logic).
Declaring capabilities on your own components
A project that publishes a library can declare the capabilities of its own variants. Capabilities live on consumable configurations (apiElements and runtimeElements of a
Java library). The tidepool build gains a second implementation of the model, tidepool-model-lite, which is an alternative to tidepool-model: both provide the capability
io.tidepool:tide-api. One rule from the documentation applies: as soon as any explicit capability is declared, all of them must be, including the implicit one, so each
project lists its own coordinates as well.
The settings file includes the new project:
rootProject.name = "tidepool"
dependencyResolutionManagement {
repositoriesMode = RepositoriesMode.FAIL_ON_PROJECT_REPOS
repositories {
mavenCentral()
}
}
include("tidepool-model", "tidepool-model-lite", "tidepool-engine", "tidepool-cli")
rootProject.name = 'tidepool'
dependencyResolutionManagement {
repositoriesMode = RepositoriesMode.FAIL_ON_PROJECT_REPOS
repositories {
mavenCentral()
}
}
include 'tidepool-model', 'tidepool-model-lite', 'tidepool-engine', 'tidepool-cli'
and both projects carry the same two capabilities (the file of tidepool-model-lite differs only in the name):
plugins {
`java-library`
}
// Once a capability is declared explicitly, the implicit one (the project's own
// group:name:version) must be declared too.
configurations {
apiElements {
outgoing {
capability("${project.group}:tidepool-model:${project.version}")
capability("${project.group}:tide-api:${project.version}")
}
}
runtimeElements {
outgoing {
capability("${project.group}:tidepool-model:${project.version}")
capability("${project.group}:tide-api:${project.version}")
}
}
}
plugins {
id 'java-library'
}
// Once a capability is declared explicitly, the implicit one (the project's own
// group:name:version) must be declared too.
configurations {
apiElements {
outgoing {
capability("${project.group}:tidepool-model:${project.version}")
capability("${project.group}:tide-api:${project.version}")
}
}
runtimeElements {
outgoing {
capability("${project.group}:tidepool-model:${project.version}")
capability("${project.group}:tide-api:${project.version}")
}
}
}
plugins {
`java-library`
}
// Once a capability is declared explicitly, the implicit one (the project's own
// group:name:version) must be declared too.
configurations {
apiElements {
outgoing {
capability("${project.group}:tidepool-model-lite:${project.version}")
capability("${project.group}:tide-api:${project.version}")
}
}
runtimeElements {
outgoing {
capability("${project.group}:tidepool-model-lite:${project.version}")
capability("${project.group}:tide-api:${project.version}")
}
}
}
plugins {
id 'java-library'
}
// Once a capability is declared explicitly, the implicit one (the project's own
// group:name:version) must be declared too.
configurations {
apiElements {
outgoing {
capability("${project.group}:tidepool-model-lite:${project.version}")
capability("${project.group}:tide-api:${project.version}")
}
}
runtimeElements {
outgoing {
capability("${project.group}:tidepool-model-lite:${project.version}")
capability("${project.group}:tide-api:${project.version}")
}
}
}
The cli build file depends on the engine, which depends on the model, and on the lite implementation:
plugins {
application
}
dependencies {
implementation(project(":tidepool-engine"))
implementation(project(":tidepool-model-lite"))
implementation("info.picocli:picocli:4.7.7")
}
application {
mainClass = "io.tidepool.cli.Main"
}
plugins {
id 'application'
}
dependencies {
implementation project(':tidepool-engine')
implementation project(':tidepool-model-lite')
implementation 'info.picocli:picocli:4.7.7'
}
application {
mainClass = 'io.tidepool.cli.Main'
}
./gradlew --no-configuration-cache :tidepool-cli:compileJava
FAILURE: Build failed with an exception.
* What went wrong:
Could not determine the dependencies of task ':tidepool-cli:compileJava'.
> Could not resolve all dependencies for configuration ':tidepool-cli:compileClasspath'.
> Could not resolve project ':tidepool-model-lite'.
Required by:
project ':tidepool-cli'
> Module 'io.tidepool:tidepool-model-lite' has been rejected:
Cannot select module because of conflict with project ':tidepool-model' (apiElements). Both provide capability 'io.tidepool:tide-api:1.0.0'.
> Could not resolve project ':tidepool-model'.
Required by:
project ':tidepool-cli' > project ':tidepool-engine'
> Module 'io.tidepool:tidepool-model' has been rejected:
Cannot select module because of conflict with project ':tidepool-model-lite' (apiElements). Both provide capability 'io.tidepool:tide-api:1.0.0'.
The conflict is between two projects, and the message says so (project ':tidepool-model'). It names the capability and, once more, shows that conflicts on a capability do
not care whether the two providers are published or built locally. Choosing the model project resolves it, and the program runs:
plugins {
application
}
configurations.configureEach {
resolutionStrategy.capabilitiesResolution.withCapability("io.tidepool:tide-api") {
val full = candidates.first {
val id = it.id
id is ProjectComponentIdentifier && id.projectPath == ":tidepool-model"
}
select(full)
because("tidepool-model is the reference implementation of tide-api")
}
}
dependencies {
implementation(project(":tidepool-engine"))
implementation(project(":tidepool-model-lite"))
implementation("info.picocli:picocli:4.7.7")
}
application {
mainClass = "io.tidepool.cli.Main"
}
plugins {
id 'application'
}
configurations.configureEach {
resolutionStrategy.capabilitiesResolution.withCapability('io.tidepool:tide-api') {
def full = candidates.find {
it.id instanceof ProjectComponentIdentifier && it.id.projectPath == ':tidepool-model'
}
select(full)
because 'tidepool-model is the reference implementation of tide-api'
}
}
dependencies {
implementation project(':tidepool-engine')
implementation project(':tidepool-model-lite')
implementation 'info.picocli:picocli:4.7.7'
}
application {
mainClass = 'io.tidepool.cli.Main'
}
./gradlew -q :tidepool-cli:dependencies --configuration runtimeClasspath
./gradlew -q :tidepool-cli:dependencyInsight --dependency tidepool-model --configuration runtimeClasspath
./gradlew :tidepool-cli:run --args="../data/stations.csv SF"
runtimeClasspath - Runtime classpath of source set 'main'.
+--- project ':tidepool-engine'
| +--- project ':tidepool-model'
| \--- org.apache.commons:commons-csv:1.14.1
| +--- commons-io:commons-io:2.20.0
| \--- commons-codec:commons-codec:1.19.0
+--- project ':tidepool-model-lite' -> project ':tidepool-model'
\--- info.picocli:picocli:4.7.7
project ':tidepool-model'
Variant runtimeElements:
Selection reasons:
- By conflict resolution: On capability io.tidepool:tide-api tidepool-model is the reference implementation of tide-api
project ':tidepool-model'
\--- project ':tidepool-engine'
\--- runtimeClasspath
project ':tidepool-model-lite' -> project ':tidepool-model'
\--- runtimeClasspath
> Task :tidepool-cli:processResources NO-SOURCE
> Task :tidepool-model:processResources NO-SOURCE
> Task :tidepool-engine:processResources NO-SOURCE
> Task :tidepool-model:compileJava UP-TO-DATE
> Task :tidepool-model:classes UP-TO-DATE
> Task :tidepool-model:jar UP-TO-DATE
> Task :tidepool-engine:compileJava
> Task :tidepool-engine:classes
> Task :tidepool-cli:compileJava UP-TO-DATE
> Task :tidepool-cli:classes UP-TO-DATE
> Task :tidepool-engine:jar UP-TO-DATE
> Task :tidepool-cli:run
San Francisco: -0,869 m
BUILD SUCCESSFUL in 516ms
6 actionable tasks: 2 executed, 4 up-to-date
The project ':tidepool-model-lite' -> project ':tidepool-model' line is the replacement, and the last command ran the application on the model project’s classes. The
outgoingVariants report shows the capabilities of a project’s consumable configurations, which is where to look when a conflict message surprises you:
./gradlew -q :tidepool-model:outgoingVariants --variant apiElements
--------------------------------------------------
Variant apiElements
--------------------------------------------------
API elements for the 'main' feature.
Capabilities
- io.tidepool:tide-api:1.0.0
- io.tidepool:tidepool-model:1.0.0
Attributes
- org.gradle.category = library
- org.gradle.dependency.bundling = external
- org.gradle.jvm.version = 26
- org.gradle.libraryelements = jar
- org.gradle.usage = java-api
Artifacts
- build/libs/tidepool-model-1.0.0.jar (artifactType = jar)
(The report goes on with the secondary variants; they were left out.) Since Gradle 8.0 (deprecated before) it is an error for two consumable configurations of one project to have the same attributes and the same capabilities, because a consumer could not tell them apart; a capability is part of what makes a variant unique.
Publishing capabilities
Capabilities travel in Gradle Module Metadata and have no place in a POM or an ivy.xml. Publishing the model project to a repository under the scratch directory (a
file: repository, see the local repository in the build file below) shows both facts:
plugins {
`java-library`
`maven-publish`
}
// Once a capability is declared explicitly, the implicit one (the project's own
// group:name:version) must be declared too.
configurations {
apiElements {
outgoing {
capability("${project.group}:tidepool-model:${project.version}")
capability("${project.group}:tide-api:${project.version}")
}
}
runtimeElements {
outgoing {
capability("${project.group}:tidepool-model:${project.version}")
capability("${project.group}:tide-api:${project.version}")
}
}
}
publishing {
publications {
create<MavenPublication>("maven") {
from(components["java"])
}
}
repositories {
maven {
name = "local"
url = uri(layout.buildDirectory.dir("repo"))
}
}
}
plugins {
id 'java-library'
id 'maven-publish'
}
// Once a capability is declared explicitly, the implicit one (the project's own
// group:name:version) must be declared too.
configurations {
apiElements {
outgoing {
capability("${project.group}:tidepool-model:${project.version}")
capability("${project.group}:tide-api:${project.version}")
}
}
runtimeElements {
outgoing {
capability("${project.group}:tidepool-model:${project.version}")
capability("${project.group}:tide-api:${project.version}")
}
}
}
publishing {
publications {
create('maven', MavenPublication) {
from components.java
}
}
repositories {
maven {
name = 'local'
url = uri(layout.buildDirectory.dir('repo'))
}
}
}
./gradlew :tidepool-model:publishAllPublicationsToLocalRepository
Maven publication 'maven' pom metadata warnings (silence with 'suppressPomMetadataWarningsFor(variant)'):
- Variant apiElements:
- Declares capability io.tidepool:tide-api:1.0.0 which cannot be mapped to Maven
- Variant runtimeElements:
- Declares capability io.tidepool:tide-api:1.0.0 which cannot be mapped to Maven
These issues indicate information that is lost in the published 'pom' metadata file, which may be an issue if the published library is consumed by an old Gradle version or Apache Maven.
The 'module' metadata file, which is used by Gradle 6+ is not affected.
Gradle warns that the POM cannot carry the capability, and says that the .module file, read by Gradle 6 and later, is not affected. The warning appeared four times in the console
(once for each task that renders the metadata); the listing shows one. The generated tidepool-model-1.0.0.module has the capabilities on each variant:
"capabilities": [
{
"group": "io.tidepool",
"name": "tide-api",
"version": "1.0.0"
},
{
"group": "io.tidepool",
"name": "tidepool-model",
"version": "1.0.0"
}
]
A Maven or an old Gradle consumer of that library will not see the second capability, and will not get the conflict detection. Publishing in depth, including how to silence
the warning with suppressPomMetadataWarningsFor, is on Publishing and
Variant-Aware Resolution & Module Metadata.
Excluding transitive dependencies
An exclusion removes a module from the graph. It is the bluntest of the tools, and the one that is easiest to overuse: the library that declared the dependency needed it, and removing it is a decision that only a test can validate. The documentation lists the legitimate reasons: a library includes dependencies your application does not need, you want to shrink what you ship, or a conflict can only be solved by not having the module. For the last case, constraints, a metadata rule or a capability are usually better.
Excluding from one dependency
exclude takes a group and a module, either of which may be omitted, and applies to the dependency it is declared on. The cli build file below excludes commons-codec
from its own declaration of Commons CSV:
plugins {
application
}
dependencies {
implementation(project(":tidepool-engine"))
implementation("info.picocli:picocli:4.7.7")
implementation("org.apache.commons:commons-csv:1.14.1") {
exclude(group = "commons-codec", module = "commons-codec")
}
}
application {
mainClass = "io.tidepool.cli.Main"
}
plugins {
id 'application'
}
dependencies {
implementation project(':tidepool-engine')
implementation 'info.picocli:picocli:4.7.7'
implementation('org.apache.commons:commons-csv:1.14.1') {
exclude group: 'commons-codec', module: 'commons-codec'
}
}
application {
mainClass = 'io.tidepool.cli.Main'
}
runtimeClasspath - Runtime classpath of source set 'main'.
+--- project ':tidepool-engine'
| +--- project ':tidepool-model'
| \--- org.apache.commons:commons-csv:1.14.1
| +--- commons-io:commons-io:2.20.0
| \--- commons-codec:commons-codec:1.19.0
+--- info.picocli:picocli:4.7.7
\--- org.apache.commons:commons-csv:1.14.1 (*)
Commons Codec is still there, under commons-csv. The exclusion changed nothing visible, because the tidepool-engine project declares Commons CSV without it, and an
exclusion applies only if every declaration that leads to the module agrees: if another dependency in the graph still needs the module, Gradle keeps it. The Javadoc of
ModuleDependency.exclude says the same and adds the remedy: to guarantee that a transitive dependency is excluded from a whole classpath, use a per-configuration exclude
rule, which is the next form.
Excluding from a whole configuration
An exclude on a configuration applies to everything that is resolved through it. With configureEach it applies to all the configurations of the project:
plugins {
application
}
// Applies to every configuration of tidepool-cli, whichever dependency brings the module in.
configurations.configureEach {
exclude(group = "commons-codec", module = "commons-codec")
}
dependencies {
implementation(project(":tidepool-engine"))
implementation("info.picocli:picocli:4.7.7")
}
application {
mainClass = "io.tidepool.cli.Main"
}
plugins {
id 'application'
}
// Applies to every configuration of tidepool-cli, whichever dependency brings the module in.
configurations.configureEach {
exclude group: 'commons-codec', module: 'commons-codec'
}
dependencies {
implementation project(':tidepool-engine')
implementation 'info.picocli:picocli:4.7.7'
}
application {
mainClass = 'io.tidepool.cli.Main'
}
./gradlew -q :tidepool-cli:dependencies --configuration runtimeClasspath
./gradlew -q :tidepool-cli:run --args="../data/stations.csv SF"
runtimeClasspath - Runtime classpath of source set 'main'.
+--- project ':tidepool-engine'
| +--- project ':tidepool-model'
| \--- org.apache.commons:commons-csv:1.14.1
| \--- commons-io:commons-io:2.20.0
\--- info.picocli:picocli:4.7.7
The dependency is gone from the graph, and the exclusion followed the module on every path. This is also where the cost of exclusions shows: the application compiled without complaint, but the engine’s use of Commons CSV needs Commons Codec at run time, and the program fails as soon as it reads the file:
Exception in thread "main" java.lang.NoClassDefFoundError: org/apache/commons/codec/binary/Base64OutputStream
at io.tidepool.engine.StationReader.read(StationReader.java:19)
at io.tidepool.cli.Main.call(Main.java:24)
at io.tidepool.cli.Main.call(Main.java:15)
at picocli.CommandLine.executeUserObject(CommandLine.java:2031)
at picocli.CommandLine.access$1500(CommandLine.java:148)
at picocli.CommandLine$RunLast.executeUserObjectOfLastSubcommandWithSameParent(CommandLine.java:2469)
at picocli.CommandLine$RunLast.handle(CommandLine.java:2461)
...
Caused by: java.lang.ClassNotFoundException: org.apache.commons.codec.binary.Base64OutputStream
...
FAILURE: Build failed with an exception.
* What went wrong:
Execution failed for task ':tidepool-cli:run' (registered by plugin 'org.gradle.application').
> Process 'command '/Library/Java/JavaVirtualMachines/jdk-26.jdk/Contents/Home/bin/java'' finished with non-zero exit value 1
(The stack trace was cut after its first seven frames, the tail ... 12 more of the cause was replaced by ..., and the generic hints at the end were cut.) The failure is a NoClassDefFoundError at run time, not a resolution error, because
nothing in the graph told Gradle that the exclusion was wrong. The same happens with the third form of exclusion, a dependency that is not transitive at all. In tidepool-engine:
plugins {
`java-library`
}
dependencies {
api(project(":tidepool-model"))
implementation("org.apache.commons:commons-csv:1.14.1") {
isTransitive = false
}
}
plugins {
id 'java-library'
}
dependencies {
api project(':tidepool-model')
implementation('org.apache.commons:commons-csv:1.14.1') {
transitive = false
}
}
the dependencies of Commons CSV (both of them) are gone from the runtime classpath of every consumer of the engine, and the program fails the same way, this time in Commons IO:
runtimeClasspath - Runtime classpath of source set 'main'.
+--- project ':tidepool-engine'
| +--- project ':tidepool-model'
| \--- org.apache.commons:commons-csv:1.14.1
\--- info.picocli:picocli:4.7.7
Exception in thread "main" java.lang.NoClassDefFoundError: org/apache/commons/io/output/AppendableOutputStream
at io.tidepool.engine.StationReader.read(StationReader.java:19)
at io.tidepool.cli.Main.call(Main.java:24)
at io.tidepool.cli.Main.call(Main.java:15)
at picocli.CommandLine.executeUserObject(CommandLine.java:2031)
at picocli.CommandLine.access$1500(CommandLine.java:148)
at picocli.CommandLine$RunLast.executeUserObjectOfLastSubcommandWithSameParent(CommandLine.java:2469)
at picocli.CommandLine$RunLast.handle(CommandLine.java:2461)
...
Caused by: java.lang.ClassNotFoundException: org.apache.commons.io.output.AppendableOutputStream
...
FAILURE: Build failed with an exception.
* What went wrong:
Execution failed for task ':tidepool-cli:run' (registered by plugin 'org.gradle.application').
> Process 'command '/Library/Java/JavaVirtualMachines/jdk-26.jdk/Contents/Home/bin/java'' finished with non-zero exit value 1
isTransitive = false (written transitive = false in the Groovy DSL) is an exclusion of everything below a dependency; use it for a dependency that is genuinely a single
JAR, or for a project dependency whose own dependencies must not leak.
Source: Configuration.exclude and ModuleDependency.setTransitive
Choosing the right tool
The documentation closes its guide with a list of alternatives to try before an exclusion: a constraint if the problem is a version, a component metadata rule if the metadata of the library is wrong (for example, a compile-time dependency that was published as a runtime one), and a capability if two modules are alternatives to each other. An exclusion remains right for the case it was designed for: a module that is on the classpath and should not be, and that no one needs.
Maven’s <exclusions> element is the same device with the same limits (it applies per dependency; Maven also accepts the wildcard * for group and artifact), and Ivy has
<exclude>; both are covered in
Maven Dependencies, Scopes & Mediation and
Ivy: Module Descriptors & Configurations.
Reading a resolution failure
The failures on this page follow a few shapes. This table is a quick way to map one to its cause; each message was produced by a run above.
| What Gradle printed | What it means | What to do |
|---|---|---|
|
|
Pick one with a constraint or |
|
Two strict declarations at the same level ask for versions that exclude each other. |
Make one of them non-strict, or change one of the versions. |
|
Two modules provide one capability. |
Choose a provider with |
|
The request for |
Check |
|
The request for 1.0 was resolved to 2.0 by conflict resolution, a constraint or a rule. |
Read the selection reasons. |
A |
The excluded module was needed at run time. |
Remove the exclusion, or provide the module another way. |
Release history
The page relies on features that arrived over many releases. The dates and the changes come from the Gradle release notes; the 9.0.0 line is the result of searching the upgrading guide for conflict, capability, constraint, exclude and resolutionStrategy.
| Release | Change relevant to this page |
|---|---|
Gradle 1.0 (2012-06-12) |
Gradle’s own dependency resolution engine, moving away from Ivy. |
Gradle 4.6 (2018-02-28) |
Dependency constraints for transitive dependencies; import of BOMs and optional dependencies from POMs. |
Gradle 5.3 (2019-03-20) |
A dependency can require a capability ( |
Gradle 6.0 (2019-11-08) |
Component capabilities for detecting and resolving conflicts between mutually exclusive dependencies; Gradle Module Metadata published by default; rich versions stable. |
Gradle 8.0 (2023-02-13) |
Two consumable configurations of one project with identical attributes and capabilities became an error (deprecated in the 7.x line). |
Gradle 8.4 (2023-10-04) |
The role-focused factory methods |
Gradle 9.0.0 (2025-07-31) |
No change to conflict resolution, constraints, capabilities or exclusions is listed in the upgrading guide (searched on 2026-10-16). |
Gradle 9.7.0 (2026-08-06) |
Configuration-cache-safe dependency-resolution APIs in tasks (see the configuration cache page). |
The release dates were read from the Gradle versions service (services.gradle.org/versions/all).
Source: Gradle 4.6 release notes, Gradle 5.3 release notes, Gradle 6.0 release notes, handling mutually exclusive dependencies, Upgrading from Gradle 7.x to 8.0, Gradle 8.4 release notes and Upgrading to Gradle 9.0.0
Gradle 8.14 and 9.8
Not run here: Gradle 8.14.x was not available for this page. Going by the documentation, the resolution behaviour described above (highest version wins, base-version
comparison, strictly, capabilities) is the same in 8.14, because none of it appears in the 9.0 upgrading guide. What differs is the way the examples are written: the
dependencyScope and resolvable factory methods used in the version ordering experiment exist since 8.4.
Where the books are outdated
Gradle in Action (Muschko, Manning 2014) is written against Gradle 1.7. Its chapter on dependency management introduces the same ideas as this page: the newest version wins,
forcing a version, failing on a conflict, and the dependencyInsight report. The ideas survived; the vocabulary around them changed.
| Book statement or practice | Current status | Source |
|---|---|---|
Dependencies are declared on |
Removed in Gradle 7.0; the configurations are |
|
Newest version wins, |
Still the default rule and still available. The Kotlin DSL (Gradle 5.0) spells the same calls with |
|
Exclusions and the |
Constraints (4.6), rich versions and |
|
The build is Groovy only, and |
The |