What does 'dependency version alignment' mean in Gradle, and why might you need it for a dependency family like Jackson?
answer
- family released together
- mixed versions break at runtime
- BOM or virtual platform
- belongsTo(..., true)
- conflict resolution propagates to siblings
basics
~10 sAlignment 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 sA 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 linesdependencies {
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
Know the symptom (mixed family versions break at runtime) and that Gradle can force them to one version.
Explain both routes — published platform vs. virtual platform via ComponentMetadataRule — and when each applies.
Discuss how conflict resolution interacts with alignment and how to debug a misaligned family with the dependencies report.
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.