skip to content

Dependency Version Alignment

Aligning a family of related modules onto a single version through a virtual platform rule or a published BOM. Asked because mismatched Jackson or Kotlin module versions are a recurring production bug.

on this pageshow

questions

5

What does 'dependency version alignment' mean in Gradle, and why might you need it for a dependency family like Jackson?

level: juniorimportance: must knowfreq 55%

answer

  1. family released together
  2. mixed versions break at runtime
  3. BOM or virtual platform
  4. belongsTo(..., true)
  5. conflict resolution propagates to siblings

basics

~10 s

Alignment forces all modules of one library family (e.g. all jackson-* artifacts) to resolve to the same version, instead of a mix, avoiding runtime incompatibilities between modules built to be released together.

solid answer

~40 s

A library family like Jackson ships many modules (`jackson-core`, `jackson-databind`, `jackson-annotations`, `jackson-module-kotlin`) that are released together and only tested against their matching siblings. With transitive dependencies, your graph can pull e.g. `databind:2.15` but `core:2.13`, which can fail at runtime (NoSuchMethodError, deserialization breakage). **Alignment** tells Gradle to treat those modules as a group and resolve them all to one consistent version chosen by conflict resolution. You achieve it either by depending on a **published BOM/platform** the vendor provides (e.g. `jackson-bom`), or, when none exists, by writing a `ComponentMetadataRule` that calls `belongsTo('group:virtual-platform:version', true)` to declare a *virtual platform*. Both make Gradle bump the whole family together rather than letting modules drift apart.

code

kotlin · 11 lines
kotlin
dependencies {
    components.all<JacksonAlignmentRule>()
}

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}", true)
        }
    }
}

go deeper

for a junior

Know the symptom (mixed family versions break at runtime) and that Gradle can force them to one version.

for a middle

Explain both routes — published platform vs. virtual platform via ComponentMetadataRule — and when each applies.

for a senior

Discuss how conflict resolution interacts with alignment and how to debug a misaligned family with the dependencies report.

for a principal

Frame as supply-chain consistency policy across many modules; decide whether to mandate vendor BOMs or maintain in-house alignment rules org-wide.

## The problem Many libraries are published as a *family* of separate Maven modules that share a release cadence and version number. Jackson is the classic example: `com.fasterxml.jackson.core:jackson-core`, `:jackson-databind`, `:jackson-annotations`, plus modules like `jackson-module-kotlin`. These are built and tested *together* — `databind:2.15` expects `core:2.15`. Mixing versions can produce `NoSuchMethodError`, `AbstractMethodError`, or subtle serialization bugs. In a real graph, different transitive dependencies request different members of the family. Library A drags in `jackson-databind:2.15.0`; library B drags in `jackson-core:2.13.3`. Gradle resolves each *module* independently, so without alignment you can ship `databind:2.15.0` next to `core:2.13.3` — a broken combination. ## What alignment does Alignment makes Gradle treat the whole family as a unit: pick the highest requested version of *any* member, then drag *all* members to that version. So requesting `core:2.13` and `databind:2.15` yields `core:2.15` + `databind:2.15`. ## Two ways to get it **1. A published platform/BOM.** Modern vendors publish a Gradle Module Metadata platform (or a Maven BOM) that lists the family with consistent constraints. You add it with `platform(...)`: ```kotlin dependencies { implementation(platform("com.fasterxml.jackson:jackson-bom:2.15.2")) implementation("com.fasterxml.jackson.core:jackson-databind") } ``` **2. A virtual platform via a metadata rule.** When the vendor publishes *no* platform, you synthesize one. A `ComponentMetadataRule` runs against each published module and calls `belongsTo("group:family-platform:version", true)` — the `true` flag marks it *virtual* (Gradle invents the platform; nobody published it). Every module that says it belongs to the same virtual platform coordinate is then aligned together. ## Why `belongsTo` virtual is powerful It lets you align families that never shipped a BOM, and align across groups when needed. The rule is applied lazily during resolution; you don't have to enumerate concrete versions — Gradle's normal conflict resolution still picks the winner, alignment just propagates that winner to siblings.

  • How does Gradle choose which single version the whole family aligns to?
    Normal conflict resolution picks the highest requested version among any family member (unless constrained otherwise); alignment then forces all siblings to that same version.
  • What's the difference between alignment and just declaring a BOM?
    A BOM supplies version constraints; alignment guarantees the *whole family moves together* even if a transitive dep bumps just one member. A published platform/BOM that declares alignment does both; a plain constraints-only BOM does not force lockstep.

Like a matched set of gears from one gearbox: swap in a single gear from a different model year and the whole transmission can grind — you replace them as a set.

saying these in an interview costs you the question

  • Saying alignment changes your *direct* declared versions — it operates during resolution on the whole graph including transitives.
  • Claiming you must pin every module's version by hand — that's exactly what alignment avoids.

context

open as a page

How do you align a dependency family that publishes no BOM, using a ComponentMetadataRule and belongsTo with a virtual platform?

level: middleimportance: must knowfreq 45%

basics

~10 s

Write a ComponentMetadataRule, register it with components.all(...), and in execute() call details.belongsTo("group:family-platform:version", true). The true flag makes it a virtual (Gradle-synthesized) platform so all matching modules align.

open as a page

How do you publish a java-platform that enforces alignment so consumers resolve a family to one consistent version?

level: middleimportance: should knowfreq 35%

basics

~10 s

Apply the java-platform plugin, list the family modules under constraints in dependencies, and publish it. Consumers add platform("you:platform:v"). Because all constraints share the platform version, the family aligns when consumers omit explicit versions.

open as a page

A Jackson family ends up with mixed versions despite an alignment rule. How do you diagnose why alignment didn't take effect?

level: seniorimportance: should knowfreq 25%

basics

~20 s

Run ./gradlew :app:dependencies (or dependencyInsight) for the configuration and look for the virtual platform node and each module's selected version + 'selected by rule/conflict' reasons. Usually the rule's group filter missed a module, or the version coordinate didn't match.

open as a page

Compare aligning a family via a virtual platform (belongsTo rule) versus a published java-platform BOM. When would you pick each?

level: seniorimportance: should knowfreq 30%

basics

~20 s

Virtual platform (belongsTo rule) needs no publishing and aligns even families that shipped no BOM, but lives in each build. A published java-platform is shareable and versioned across many repos but you must author and release it.

open as a page