skip to content

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

level: seniorimportance: should knowfreq 45%

answer

  1. constraints don't add the module
  2. highest-version-wins
  3. platform = floor/recommend
  4. enforcedPlatform = strict, can downgrade
  5. dependencyInsight to diagnose

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.

solid answer

~50 s

When you import a BOM with `platform()`, each managed version becomes a **constraint** — a soft 'prefer' that joins Gradle's graph. Gradle's resolution still selects the **highest** version among all requests and constraints for a module, so a transitive needing a newer version, or an explicit version you declare, can override the BOM. Constraints only set or raise the chosen version; they never add the module to the graph by themselves. With `enforcedPlatform()` the constraints become **strict** versions, which dominate and can downgrade conflicting requests. You can layer your own `constraints { }` on top to bump specific entries the BOM pins too low, and you can inspect the outcome with `./gradlew dependencyInsight --dependency <module>`, which shows whether the version 'came from' the platform constraint, a transitive, or an explicit declaration, and why it was selected.

code

bash · 4 lines
bash
# See why a BOM-governed module resolved to a given version
./gradlew dependencyInsight \
  --dependency jackson-databind \
  --configuration runtimeClasspath

go deeper

for a junior

Know that BOM versions can be overridden by explicit or higher transitive versions with plain platform().

for a middle

Explain constraints vs dependencies and highest-version-wins resolution.

for a senior

Detail strict-version behavior of enforcedPlatform, local-constraint overrides, and using dependencyInsight to diagnose selection.

for a principal

Define resolution-policy standards (when to enforce, how to surface conflicts) and tooling to audit selected versions fleet-wide.

## Constraints vs dependencies A **dependency** says "put this module on the graph at (around) this version." A **constraint** says "*if* this module is on the graph, prefer/force this version" — it never pulls the module in by itself. Importing a BOM via `platform()` turns the BOM's `<dependencyManagement>` entries into constraints attached to the configuration. ## Default resolution Gradle uses **highest-version-wins** conflict resolution. For a given module it gathers every request and constraint, then picks the highest compatible version. Consequences with a plain `platform()`: - **BOM only**: the BOM version is used. - **Transitive asks higher**: the higher transitive version wins; the BOM constraint is satisfied (the chosen version is >= it). - **You declare explicit**: your explicit version participates and, if higher, wins. So plain BOM constraints are a *floor/recommendation*, not a ceiling. ## enforcedPlatform — strict `enforcedPlatform()` marks every constraint as **strict** (`require`/`strictly`). Strict versions reject anything outside their range, so the BOM version dominates and can **downgrade** a transitive. If two strict constraints disagree, resolution **fails** with a clear conflict error rather than silently choosing. ## Overriding a BOM entry You rarely need to fork the BOM. Add a local constraint: ```kotlin dependencies { implementation(platform("org.springframework.boot:spring-boot-dependencies:3.2.0")) constraints { // bump just one library the BOM pins too low implementation("com.fasterxml.jackson.core:jackson-databind:2.17.1") } } ``` Because your local constraint is higher, it wins under default resolution. ## Diagnosing Use the dependencyInsight report: ```bash ./gradlew dependencyInsight --dependency jackson-databind --configuration runtimeClasspath ``` It prints the **selected** version, the **requested** versions, and the **reasons** (e.g. "by constraint", "by conflict resolution"), telling you whether the platform, a transitive, or an explicit declaration determined the result. ## Practical guidance - Prefer `platform()`; it cooperates with legitimate upgrades. - Reach for `enforcedPlatform()` only to reproduce Maven or to hard-lock a stack you own the risk for. - Layer targeted constraints instead of pinning explicit versions everywhere — it keeps intent clear and conflicts visible.

  • How do you override a single version pinned by an imported BOM without forking the BOM?
    Add a local constraint (or an explicit version) that is higher than the BOM's value. Under Gradle's default highest-version-wins resolution your constraint is selected. A dependencyInsight report confirms which source won.
  • What happens if two enforcedPlatform imports declare conflicting strict versions for the same module?
    Resolution fails with an explicit conflict error, because strict versions cannot both be satisfied. Plain platform() would instead silently pick the highest compatible version.
  • Does a BOM constraint ever pull an unused module onto the classpath?
    No. A constraint only governs the version of a module that is otherwise requested; it never introduces the module itself.

saying these in an interview costs you the question

  • Believing plain platform() acts as a ceiling that caps versions — it's a recommendation/floor.
  • Thinking BOM constraints add modules to the graph.
  • Forgetting that enforcedPlatform conflicts fail the build rather than auto-resolving.

context