Gradle Dependency Management
Gradle dependency management revolves around three core concepts: repositories, configurations, and dependency declarations. Repositories determine where artifacts are downloaded from. Configurations define the scope in which a dependency applies, such as compilation, runtime, or testing. Dependency declarations specify the exact coordinates or project references to resolve.
A typical Java project setup looks like this:
plugins {
id 'java-library'
}
repositories {
mavenCentral()
}
dependencies {
implementation 'com.google.guava:guava:33.0.0-jre'
testImplementation 'org.junit.jupiter:junit-jupiter:5.10.2'
}
Gradle supports several repository types, including Maven Central, Google Maven, local Maven repositories, and custom Maven endpoints. Repositories are searched in declaration order, and Gradle stops at the first match.
repositories {
mavenCentral()
google()
maven {
url = uri("https://maven.aliyun.com/repository/public")
}
mavenLocal()
}
Use mavenLocal() cautiously, as it can introduce non-reproducible builds tied to a specific machine.
For internal networks, teams usually configure private repositories with authentication. In multi-module builds, it is best to centralize repository management in settings.gradle under dependencyResolutionManagement so every submodule shares the same resolution rules.
Dependency Configurations and Scopes
The Java plugin exposes multiple configurations. Choosing the right scope prevents dependency leakage and keeps artifacts lean.
- implementation: Available for compilation and runtime within the current module, but not exposed to consumers.
- api: Available for compilation and runtime, and transitively exposed to downstream consumers. Requires the
java-libraryplugin. - compileOnly: Needed only during compilation; excluded from runtime and packaging.
- runtimeOnly: Needed only at runtime; not visible during compilation.
- testImplementation: Used for compiling and running tests.
- annotationProcessor: Reserved for annotation processors such as Lombok.
Example:
dependencies {
implementation 'com.google.guava:guava:33.0.0-jre'
api 'org.apache.commons:commons-lang3:3.14.0'
compileOnly 'org.projectlombok:lombok:1.18.30'
annotationProcessor 'org.projectlombok:lombok:1.18.30'
runtimeOnly 'org.postgresql:postgresql:42.7.3'
testImplementation 'org.junit.jupiter:junit-jupiter:5.10.2'
}
Prefer implementation over the deprecated compile configuration whenever possible.
Dependency Coordinates and Dynamic Versions
A dependency coordinate follows the group:name:version pattern. Fixed versions are the norm, but Gradle also supports dynamic versions such as 1.+. Dynamic versions should be avoided in production because they compromise build reproducibility.
dependencies {
implementation 'com.example:lib:1.2.3'
implementation 'com.example:lib:1.+'
implementation('com.example:lib') {
version {
strictly '1.2.3'
}
}
}
For centralized version control, use a version catalog defined in gradle/libs.versions.toml:
[versions]
guava = "33.0.0-jre"
[libraries]
guava = { module = "com.google.guava:guava", version.ref = "guava" }
Reference it in build.gradle:
dependencies {
implementation libs.guava
}
Transitive Dependencies and Conflict Resolution
Gradle resolves transitive dependencies automatically. When multiple versions of the same artifact appear in the graph, the highest version wins by default. You can override this behavior with exclusions, forced versions, or dependency constraints.
To exclude a transitive dependency:
implementation('com.example:lib:1.0') {
exclude group: 'commons-logging', module: 'commons-logging'
}
To force a specific version across all configurations:
configurations.all {
resolutionStrategy {
force 'com.google.guava:guava:32.1.3-jre'
failOnVersionConflict()
}
}
Dependency constraints let you declare version requirements without adding a direct dependency:
dependencies {
constraints {
implementation('com.google.guava:guava:32.1.3-jre') {
because 'addresses a known security vulnerability'
}
}
}
Platforms and BOMs
A Bill of Materials (BOM) aggregates versions for a set of related libraries. Import it with platform:
dependencies {
implementation platform('org.springframework.boot:spring-boot-dependencies:3.2.5')
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
}
If you want the BOM to override conflicting versions rather than merely suggest them, use enforcedPlatform:
implementation enforcedPlatform('org.springframework.boot:spring-boot-dependencies:3.2.5')
Dependency Locking and Inspection
Locking dependencies guarantees reproducible builds. Enable it with:
dependencyLocking {
lockAllConfigurations()
}
Generate lock files by running:
./gradlew dependencies --write-locks
Inspect the dependency tree for a specific configuration:
./gradlew dependencies --configuration runtimeClasspath
Trace why a particular dependency was included:
./gradlew dependencyInsight --dependency guava --configuration runtimeClasspath
Best Practices
- Favor
implementationand avoid the obsoletecompileconfiguration. - Steer clear of dynamic versions and
SNAPSHOTartifacts in release builds. - Consolidate versions using BOMs or version catalogs to minimize conflicts.
- Periodically run
dependenciesanddependencyUpdatesto audit the dependency graph. - In multi-module projects, centralize repositories and version management in
settings.gradle.