Managing dependencies in a project can be challenging. This chapter describes techniques for troubleshooting issues you might encounter in your project as well as best practices for avoiding common problems.

Resolving version conflicts

Gradle resolves version conflicts by picking the highest version of a module. Build scans and the dependency insight report are immensely helpful in identifying why a specific version was selected. If the resolution result is not satisfying (e.g. the selected version of a module is too high) or it fails (because you configured ResolutionStrategy.failOnVersionConflict()) you have the following possibilities to fix it.

Using dynamic versions and changing modules

There are many situations when you want to use the latest version of a particular module dependency, or the latest in a range of versions. This can be a requirement during development, or you may be developing a library that is designed to work with a range of dependency versions. You can easily depend on these constantly changing dependencies by using a dynamic version. A dynamic version can be either a version range (e.g. 2.+) or it can be a placeholder for the latest version available e.g. latest.integration.

Alternatively, the module you request can change over time even for the same version, a so-called changing version. An example of this type of changing module is a Maven SNAPSHOT module, which always points at the latest artifact published. In other words, a standard Maven snapshot is a module that is continually evolving, it is a "changing module".

Using dynamic versions and changing modules can lead to unreproducible builds. As new versions of a particular module are published, its API may become incompatible with your source code. Use this feature with caution!

By default, Gradle caches dynamic versions and changing modules for 24 hours. During that time frame Gradle does not contact any of the declared, remote repositories for new versions. If you want Gradle to check the remote repository more frequently or with every execution of your build, then you will need to change the time to live (TTL) threshold.

Using a short TTL threshold for dynamic or changing versions may result in longer build times due to the increased number of HTTP(s) calls.

You can override the default cache modes using command line options. You can also change the cache expiry times in your build programmatically using the resolution strategy.

Controlling dependency caching programmatically

You can fine-tune certain aspects of caching programmatically using the ResolutionStrategy for a configuration. The programmatic approach is useful if you would like to change the settings permanently.

By default, Gradle caches dynamic versions for 24 hours. To change how long Gradle will cache the resolved version for a dynamic version, use:

Example 1. Dynamic version cache control
GroovyKotlin
build.gradle
configurations.all {
    resolutionStrategy.cacheDynamicVersionsFor 10, 'minutes'
}

By default, Gradle caches changing modules for 24 hours. To change how long Gradle will cache the meta-data and artifacts for a changing module, use:

Example 2. Changing module cache control
GroovyKotlin
build.gradle
configurations.all {
    resolutionStrategy.cacheChangingModulesFor 4, 'hours'
}

Controlling dependency caching from the command line

You can control the behavior of dependency caching for a distinct build invocation from the command line. Command line options are helpful for making a selective, ad-hoc choice for a single execution of the build.

Avoiding network access with offline mode

The --offline command line switch tells Gradle to always use dependency modules from the cache, regardless if they are due to be checked again. When running with offline, Gradle will never attempt to access the network to perform dependency resolution. If required modules are not present in the dependency cache, build execution will fail.

Forcing all dependencies to be re-resolved

At times, the Gradle Dependency Cache can become out of sync with the actual state of the configured repositories. Perhaps a repository was initially misconfigured, or perhaps a "non-changing" module was published incorrectly. To refresh all dependencies in the dependency cache, use the --refresh-dependencies option on the command line.

The --refresh-dependencies option tells Gradle to ignore all cached entries for resolved modules and artifacts. A fresh resolve will be performed against all configured repositories, with dynamic versions recalculated, modules refreshed, and artifacts downloaded. However, where possible Gradle will check if the previously downloaded artifacts are valid before downloading again. This is done by comparing published SHA1 values in the repository with the SHA1 values for existing downloaded artifacts.

Locking dependency versions

The use of dynamic dependencies in a build is convenient. The user does not need to know the latest version of a dependency and Gradle automatically uses new versions once they are published. However, dynamic dependencies make builds non-reproducible, as they can resolve to a different version at a later point in time. This makes it hard to reproduce old builds when debugging a problem. It can also disrupt development if a new, but incompatible version is selected. In the best case the CI build catches the problem and someone needs to investigate. In the worst case, the problem makes it to production unnoticed.

Gradle offers dependency locking to solve this problem. The user can run a build asking to persist the resolved versions for every module dependency. This file is then checked in and the versions in it are used on all subsequent runs until the lock is updated or removed again.

Versioning of file dependencies

Legacy projects sometimes prefer to consume file dependencies instead of module dependencies. File dependencies can point to any file in the filesystem and do not need to adhere a specific naming convention. It is recommended to clearly express the intention and a concrete version for file dependencies. File dependencies are not considered by Gradle’s version conflict resolution. Therefore, it is extremely important to assign a version to the file name to indicate the distinct set of changes shipped with it. For example commons-beanutils-1.3.jar lets you track the changes of the library by the release notes.

As a result, the dependencies of the project are easier to maintain and organize. It’s much easier to uncover potential API incompatibilities by the assigned version.

Constraints on configuration resolution

Configurations need to be resolved safely when crossing project boundaries because resolving configurations can have side effects on Gradle’s project model. Gradle can usually manage this safe access, but the configuration needs to be accessed in a way that enables Gradle to do so. There are a number of ways a configuration might be resolved unsafely and Gradle will produce a deprecation warning for each unsafe access.

For example:

  • A task from one project directly resolves a configuration in another project.

  • A task specifies a configuration from another project as an input file collection.

  • A build script for a project resolves a configuration in another project during evaluation.

  • A configuration is resolved in a user-managed thread (i.e., a thread not managed by Gradle).

If your build has an unsafe access deprecation warning, it needs to be fixed. It’s a symptom of these bad practices and cause strange and indeterminate errors.

More importantly, resolving a configuration from a user-managed thread is not supported. To ensure that the configuration is resolved safely, it must be resolved in a Gradle-managed thread. Afterwards, the resolution result can be used in user-managed threads.

In most cases, the deprecation warning can be fixed by defining a configuration in the project where the resolution is occurring and setting it to extend from the configuration in the other project.