# How to Align Dependency Versions in Gradle

version 9.2.0

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 'com.fasterxml.jackson.core:jackson-databind:2.8.9'
        implementation 'io.vertx:vertx-core:3.5.3'
    }

Dependency resolution may result in:

- `jackson-core` → `2.9.5` (required by `vertx-core`)

- `jackson-databind` → `2.9.5` (resolved via conflict resolution)

- `jackson-annotations` → `2.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

    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:

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

    abstract class JacksonAlignmentRule implements ComponentMetadataRule {
        void execute(ComponentMetadataContext ctx) {
            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:

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.