How to Align Dependency Versions in Gradle
Dependency version alignment ensures that different modules belonging to the same logical group (a platform) use identical versions in the dependency graph.
Why do Inconsistent Module Versions Happen?
Gradle supports aligning versions of modules that belong to the same platform. For example, a component’s API and implementation modules should use the same version.
However, due to transitive dependency resolution, modules within the same platform may end up using different versions, leading to potential compatibility issues.
Consider the following example, where your project depends on both jackson-databind
and vert.x
:
dependencies {
implementation("com.fasterxml.jackson.core:jackson-databind:2.8.9")
implementation("io.vertx:vertx-core:3.5.3")
}
dependencies {
implementation("org.apache.commons:commons-lang3:3.2")
constraints {
implementation("org.apache.commons:commons-lang3:3.1") {
because("Version 1.3 introduces breaking changes not yet handled")
}
}
}
Dependency resolution may result in:
-
jackson-core
→2.9.5
(required byvertx-core
) -
jackson-databind
→2.9.5
(resolved via conflict resolution) -
jackson-annotations
→2.9.0
(a dependency ofjackson-databind:2.9.5
)
The issue is that Vert.x
(3.5.0
) uses an older Jackson
(2.9.0
), but the explicit dependency (2.9.5
) forces Gradle to upgrade Vert.x
\'s Jackson
dependencies from 2.9.0
to 2.9.5
:
> Task :dependencies
------------------------------------------------------------
Root project 'how_to_align_dependency_versions'
------------------------------------------------------------
runtimeClasspath - Runtime classpath of source set 'main'.
+--- com.fasterxml.jackson.core:jackson-databind:2.8.9 -> 2.9.5
| +--- com.fasterxml.jackson.core:jackson-annotations:2.9.0
| \--- com.fasterxml.jackson.core:jackson-core:2.9.5
\--- io.vertx:vertx-core:3.5.3
+--- io.netty:netty-common:4.1.19.Final
...
| \--- io.netty:netty-transport:4.1.19.Final (*)
+--- com.fasterxml.jackson.core:jackson-core:2.9.5
\--- com.fasterxml.jackson.core:jackson-databind:2.9.5 (*)
This mismatch can lead to incompatibility issues and unexpected failures.
Gradle provides dependency version alignment through platforms, ensuring related modules use consistent versions.
Option 1: Using a Published Platform
If a public platform (also known as a BOM) is available, import it as a platform:
dependencies {
implementation(platform("com.fasterxml.jackson:jackson-bom:2.8.9"))
implementation("com.fasterxml.jackson.core:jackson-databind:2.8.9")
implementation("io.vertx:vertx-core:3.5.3")
}
Running ./gradlew dependencies --configuration runtimeClasspath
showcases the aligned dependencies:
> Task :dependencies
------------------------------------------------------------
Root project 'how_to_align_dependency_versions'
------------------------------------------------------------
runtimeClasspath - Runtime classpath of source set 'main'.
+--- com.fasterxml.jackson:jackson-bom:2.8.9
| +--- com.fasterxml.jackson.core:jackson-databind:2.8.9 -> 2.9.5 (c)
| +--- com.fasterxml.jackson.core:jackson-core:2.8.9 -> 2.9.5 (c)
| \--- com.fasterxml.jackson.core:jackson-annotations:2.8.0 -> 2.9.0 (c)
+--- com.fasterxml.jackson.core:jackson-databind:2.8.9 -> 2.9.5
| +--- com.fasterxml.jackson.core:jackson-annotations:2.9.0
| \--- com.fasterxml.jackson.core:jackson-core:2.9.5
\--- io.vertx:vertx-core:3.5.3
+--- io.netty:netty-common:4.1.19.Final
...
| \--- io.netty:netty-transport:4.1.19.Final (*)
+--- com.fasterxml.jackson.core:jackson-core:2.9.5
\--- com.fasterxml.jackson.core:jackson-databind:2.9.5 (*)
Option 2: Creating a Virtual Platform
If no public BOM exists, you can create a virtual platform.
In this case, Gradle builds the platform dynamically based on the modules being used. For this, you must define component metadata rules:
abstract class JacksonAlignmentRule : ComponentMetadataRule {
override fun execute(ctx: ComponentMetadataContext) {
ctx.details.run {
if (id.group.startsWith("com.fasterxml.jackson")) {
belongsTo("com.fasterxml.jackson:jackson-virtual-platform:${id.version}")
}
}
}
}
dependencies {
implementation("com.fasterxml.jackson.core:jackson-databind:2.8.9")
implementation("io.vertx:vertx-core:3.5.3")
dependencies {
components.all<JacksonAlignmentRule>()
}
}
This ensures that all Jackson modules align to the same version, even if brought in transitively.
Running ./gradlew dependencies --configuration runtimeClasspath
showcases the aligned dependencies:
> Task :dependencies
------------------------------------------------------------
Root project 'how_to_align_dependency_versions'
------------------------------------------------------------
runtimeClasspath - Runtime classpath of source set 'main'.
+--- com.fasterxml.jackson.core:jackson-databind:2.8.9 -> 2.9.5
| +--- com.fasterxml.jackson.core:jackson-annotations:2.9.0 -> 2.9.5
| \--- com.fasterxml.jackson.core:jackson-core:2.9.5
\--- io.vertx:vertx-core:3.5.3
+--- io.netty:netty-common:4.1.19.Final
...
| \--- io.netty:netty-transport:4.1.19.Final (*)
+--- com.fasterxml.jackson.core:jackson-core:2.9.5
\--- com.fasterxml.jackson.core:jackson-databind:2.9.5 (*)
Option 3: Using the Java Plugin
Gradle natively supports version alignment using the Java Platform Plugin.
When projects have multiple modules that are versioned together (e.g., lib
, utils
, core
), using mixed versions (e.g., core:1.0
and lib:1.1
) can lead to runtime issues or incompatibilities.
Consider a project with three modules:
-
lib
-
utils
-
core
(depends onlib
andutils
)
A consumer project declares:
-
core
version 1.0 -
lib
version 1.1
By default, Gradle selects core:1.0
and lib:1.1
, leading to version misalignment.
To fix this, introduce a platform module that enforces constraints:
plugins {
`java-platform`
}
dependencies {
// The platform declares constraints on all components that
// require alignment
constraints {
api(project(":core"))
api(project(":lib"))
api(project(":utils"))
}
}
plugins {
id 'java-platform'
}
dependencies {
// The platform declares constraints on all components that
// require alignment
constraints {
api(project(":core"))
api(project(":lib"))
api(project(":utils"))
}
}
Each module should declare a dependency on the platform:
dependencies {
// Each project has a dependency on the platform
api(platform(project(":platform")))
// And any additional dependency required
implementation(project(":lib"))
implementation(project(":utils"))
}
dependencies {
// Each project has a dependency on the platform
api(platform(project(":platform")))
// And any additional dependency required
implementation(project(":lib"))
implementation(project(":utils"))
}
This ensures all dependencies (core
, lib
, and utils
) resolve consistently to version 1.1
.
Summary
Using platforms and BOMs, Gradle ensures consistent dependency versions, avoiding compatibility issues. When no published BOM exists, virtual platforms allow manual alignment of dependencies.