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:

build.gradle.kts
dependencies {
    implementation("com.fasterxml.jackson.core:jackson-databind:2.8.9")
    implementation("io.vertx:vertx-core:3.5.3")
}
build.gradle
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-core2.9.5 (required by vertx-core)

  • jackson-databind2.9.5 (resolved via conflict resolution)

  • jackson-annotations2.9.0 (a dependency of jackson-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:

dependencies.out
> 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:

build.gradle.kts
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")
}
build.gradle

Running ./gradlew dependencies --configuration runtimeClasspath showcases the aligned dependencies:

dependencies-bom.out
> 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:

build.gradle.kts
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>()
    }
}
build.gradle

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:

dependencies-platform.out
> 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 on lib and utils)

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:

build.gradle.kts
plugins {
    `java-platform`
}

dependencies {
    // The platform declares constraints on all components that
    // require alignment
    constraints {
        api(project(":core"))
        api(project(":lib"))
        api(project(":utils"))
    }
}
build.gradle
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:

build.gradle.kts
dependencies {
    // Each project has a dependency on the platform
    api(platform(project(":platform")))

    // And any additional dependency required
    implementation(project(":lib"))
    implementation(project(":utils"))
}
build.gradle
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.