skip to content

Versions and Catalogs

Where versions are declared and how they are expressed: version catalogs, platforms and BOMs, dynamic versions, and rich version constraints. Interviewers focus here because version sprawl is what every growing build eventually fights.

on this pageshow

explore

questions

page 1 of 2

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

What is a Gradle version catalog and where does the default `libs` catalog come from?

level: juniorimportance: must knowfreq 68%

basics

~10 s

A version catalog is a central, typed list of dependency coordinates and versions shared across a build. Gradle auto-creates the libs catalog from gradle/libs.versions.toml if that file exists.

open as a page

What is the gradle/libs.versions.toml file, and what are the four tables it can contain?

level: juniorimportance: must knowfreq 70%

basics

~10 s

It's Gradle's version catalog: a TOML file at gradle/libs.versions.toml listing dependency coordinates and versions in one place. Its four tables are [versions], [libraries], [bundles], and [plugins].

open as a page

What is the difference between a dynamic version and a changing version in Gradle dependency management?

level: juniorimportance: must knowfreq 60%

basics

~20 s

A dynamic version lets Gradle pick which version to resolve (e.g. '1.+', 'latest.release'). A changing version is one fixed coordinate whose artifacts can change over time (e.g. '-SNAPSHOT'), so the same version number may hold different content.

open as a page

What does the platform() dependency notation do in Gradle, and what is a typical use for it?

level: juniorimportance: must knowfreq 60%

basics

~10 s

platform() imports a Maven BOM so its version constraints apply to your dependencies, letting you declare those dependencies without versions and keep them aligned.

open as a page

What is a rich version declaration in Gradle, and what problem does it solve compared to a plain version string?

level: juniorimportance: must knowfreq 55%

basics

~20 s

A rich version uses a version { } block to declare more than a single string — e.g. require, prefer, strictly, reject — so you can express acceptable ranges and preferences instead of just one fixed number.

open as a page

What are type-safe version-catalog accessors in Gradle, and how do you use them in a build script to declare a dependency?

level: juniorimportance: must knowfreq 70%

basics

~10 s

Gradle generates a libs object from the version catalog so you can write implementation(libs.junit) instead of a raw coordinate string. The accessor is type-safe and offers IDE auto-completion.

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

In a [libraries] entry, when would you use version.ref versus an inline version, and how does module differ from group+name?

level: middleimportance: must knowfreq 60%

basics

~10 s

Use version.ref to point a library at a shared name in [versions] so many libraries upgrade together; use inline version for a one-off. module = "group:name" is shorthand for separate group and name keys.

open as a page

How do you control how long Gradle caches dynamic versions and changing modules, and how do you force a refresh?

level: middleimportance: must knowfreq 55%

basics

~10 s

Use resolutionStrategy.cacheDynamicVersionsFor(...) and cacheChangingModulesFor(...) inside a configuration block to set TTLs. To bypass caches for one build, run with --refresh-dependencies.

open as a page

What is the difference between platform() and enforcedPlatform() in Gradle?

level: middleimportance: must knowfreq 65%

basics

~10 s

platform() applies BOM versions as recommendations that explicit or transitive versions can override; enforcedPlatform() applies them strictly, forcing those versions and overriding any conflicting declaration.

open as a page

How do you use `strictly` to force a downgrade of a transitive dependency, and what happens if another part of the graph disagrees?

level: middleimportance: must knowfreq 50%

basics

~20 s

Declare the dependency (or a constraint) with version { strictly("1.2") }. strictly is a hard upper bound, so Gradle will downgrade to it. If another node needs a version outside the strict range, the build fails with a conflict.

open as a page

How does Gradle map a catalog alias like `groovy-json` or `commons.lang3` to its generated accessor, and why does the mapping sometimes surprise people?

level: middleimportance: must knowfreq 55%

basics

~10 s

Dashes (and dots) in an alias become nested levels in the accessor: groovy-json becomes libs.groovy.json. The separators create sub-objects, so the alias a-b-c reads libs.a.b.c.

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

Inside `dependencyResolutionManagement { versionCatalogs { create(...) } }`, how do you build a catalog from a file versus programmatically, and what does `from()` accept?

level: middleimportance: should knowfreq 52%

basics

~10 s

Use from(files("...toml")) to import a TOML file, or from("group:artifact:version") to import a published catalog. Or skip from and add entries directly with version(...), library(...), bundle(...), plugin(...).

open as a page

What is the [bundles] table for, and how do bundle entries relate to [libraries] aliases?

level: middleimportance: should knowfreq 45%

basics

~10 s

A [bundles] entry is a named list of [libraries] aliases that are commonly used together, so you can add them all with one accessor instead of declaring each dependency separately.

open as a page

How does the [plugins] table work in libs.versions.toml, and how is it consumed differently from [libraries]?

level: middleimportance: should knowfreq 50%

basics

~10 s

[plugins] entries declare a plugin id and version (e.g. id = "...", version = "..."). You consume them in the plugins {} block via alias(libs.plugins.<name>), not in dependencies {}.

open as a page

What dynamic version selectors does Gradle support, and how does it choose which concrete version to resolve?

level: middleimportance: should knowfreq 40%

basics

~10 s

Gradle supports prefix wildcards ('1.+'), Maven ranges ('[1.0,2.0)'), and the keywords 'latest.release' / 'latest.integration'. It lists candidate versions, filters by the selector and component status, then picks the highest match.

open as a page

How does Gradle treat Maven -SNAPSHOT dependencies, and how can you make a non-snapshot module behave the same way?

level: middleimportance: should knowfreq 45%

basics

~20 s

Gradle automatically treats any version ending in -SNAPSHOT as a changing module — it may re-fetch the artifacts within the cache window. For a non-snapshot coordinate you opt in with isChanging = true on the dependency.

open as a page

How do you author your own BOM/platform in Gradle, and what does the constraints block do?

level: middleimportance: should knowfreq 50%

basics

~10 s

Apply the java-platform plugin in a dedicated project and declare versions in a dependencies { constraints { api(...) / runtime(...) } } block. Published, it becomes a BOM others import with platform().

open as a page

How do `reject` and `rejectAll()` work in a rich version block, and when would you use each?

level: middleimportance: should knowfreq 38%

basics

~10 s

reject lists specific versions or ranges Gradle must never resolve to — useful to blacklist a buggy or vulnerable release. rejectAll() rejects every version; on a constraint it effectively forbids the dependency from appearing.

open as a page

Explain the difference between `libs.bundles.x`, `libs.plugins.y`, and `libs.versions.z` accessors. When and how do you use each?

level: middleimportance: should knowfreq 50%

basics

~10 s

libs.bundles.x is a named group of libraries you add in one line; libs.plugins.y is a plugin alias used in the plugins {} block via alias(...); libs.versions.z returns the raw version string from the catalog.

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

How do you publish a version catalog so other builds can import it, and how does the consuming build pull it in?

level: seniorimportance: should knowfreq 41%

basics

~20 s

In a producer project apply the version-catalog plugin, define entries via the catalog { versionCatalog { ... } } extension, and publish with maven-publish (the catalog adds a versionCatalog component). Consumers import it via from("group:artifact:version") in settings.

open as a page

A team has several independent repos and wants one source of truth for dependency versions. Compare a checked-in shared `libs.versions.toml` versus a published version catalog, and when you'd pick each.

level: seniorimportance: should knowfreq 35%

basics

~20 s

A copied/checked-in TOML is simple but drifts across repos. A published catalog (version-catalog plugin) is a versioned artifact imported with from("g:a:v"), giving one governed source and controlled upgrades — at the cost of publishing infrastructure.

open as a page

What naming rules govern catalog aliases in the TOML, and what common errors do invalid names cause?

level: seniorimportance: should knowfreq 35%

basics

~20 s

Aliases use letters, digits and the separators -, _, . which all map to dotted accessors. Names must start with a letter, can't be reserved words like extensions/class/convention, and the leading segment can't collide with another alias's prefix.

open as a page

How are rich version constraints expressed inside a libs.versions.toml entry, and what do require, strictly, prefer, and reject mean there?

level: seniorimportance: should knowfreq 30%

basics

~20 s

Instead of a plain version string, an entry's version can be an object: version = { require = "..." }, strictly, prefer, or reject. require/strictly set the chosen version (strictly is a hard fail), prefer is a tie-breaker, reject excludes versions.

open as a page

Why do dynamic and changing versions threaten build reproducibility, and what mechanisms does Gradle offer to control or forbid them?

level: seniorimportance: should knowfreq 38%

basics

~10 s

They make the same source resolve different artifacts over time, so builds aren't reproducible. Gradle counters this with dependency locking (gradle.lockfile) and resolutionStrategy guards like failOnDynamicVersions() / failOnChangingVersions().

open as a page

How do BOM-imported version constraints interact with Gradle's conflict resolution and explicit/transitive versions?

level: seniorimportance: should knowfreq 45%

basics

~10 s

A platform() BOM adds constraints that participate in resolution as recommendations: Gradle still picks the highest compatible version, so transitives or explicit declarations can override the BOM. enforcedPlatform() makes them strict and dominant.

open as a page

showing 1–30 of 36