In a multi-module build importing several BOMs, how do version conflicts resolve, and how would you govern this across an organization?
answer
- Maven: first import wins (order matters)
- Gradle: constraints, highest-wins (enforced forces)
- company platform / java-platform module
- single source of truth, one upgrade lever
- Boot<->Spring Cloud train pairing
basics
~20 sWhen multiple BOMs manage the same artifact, Maven picks the first-declared entry (first-wins), while Gradle merges constraints and resolves to the highest compatible unless enforced. Govern with a single company platform/BOM module that all projects import.
solid answer
~50 sWith multiple imported BOMs (e.g. spring-boot-dependencies plus a cloud or messaging BOM) that both manage an artifact, Maven resolves by declaration order in dependencyManagement — the first matching import wins, so BOM ordering is significant and must be intentional. Gradle instead treats each platform's entries as constraints and, by default, selects the highest version satisfying all constraints (enforcedPlatform forces instead). To govern this at scale, publish an internal 'platform' — a company BOM (Maven type=pom, or a Gradle java-platform module) that itself imports spring-boot-dependencies and pins your approved third-party and internal versions. Every service imports only that one BOM, giving a single source of truth, reproducible builds, and a controlled upgrade cadence. Combine with dependency-tree verification in CI, an SCA/CVE scanner, and a policy that overrides are family-level, tracked, and time-boxed until the next platform bump.
code
kotlin · 20 lines// An internal Gradle 'java-platform' module: acme-platform/build.gradle.kts
plugins { `java-platform` }
javaPlatform { allowDependencies() } // lets us import other BOMs
dependencies {
// Adopt the whole tested Spring Boot matrix
api(platform("org.springframework.boot:spring-boot-dependencies:3.3.4"))
// Pair the matching Spring Cloud train
api(platform("org.springframework.cloud:spring-cloud-dependencies:2023.0.3"))
constraints {
// Org-approved internal libs and any CVE-driven overrides (family-level)
api("com.acme:acme-commons:4.2.0")
api("com.fasterxml.jackson:jackson-bom:2.18.0") // temporary CVE bump, tracked
}
}
// A consumer service then imports ONLY the platform:
// dependencies { implementation(platform("com.acme:acme-platform:2025.3")) }go deeper
Aware that multiple BOMs can conflict on the same library.
State Maven first-wins vs Gradle constraint-merge for conflicts.
Order imports deliberately and know enforcedPlatform pitfalls.
Design an org platform/java-platform, upgrade cadence, override policy, and CI/SCA governance; encode Boot/Cloud train pairing.
## The multi-BOM conflict question Real projects import more than one BOM: `spring-boot-dependencies`, plus e.g. `spring-cloud-dependencies`, a messaging client BOM, or a testing BOM. When two BOMs both manage `com.fasterxml.jackson.core:jackson-databind` at different versions, which wins? ### Maven — first declaration wins Maven's `dependencyManagement` uses **nearest/first-declared** semantics for imports: the **first** `<scope>import</scope>` entry that manages a given artifact sets its version; later imports for the same artifact are ignored. Therefore **the order of `<dependency>` imports in `<dependencyManagement>` is load-bearing**. Convention: put the BOM whose versions you trust most (often spring-boot-dependencies, or spring-cloud whose own version aligns with a Boot release) in the position that yields the set you want. Spring Cloud, notably, is designed so its BOM is imported *alongside* the matching Boot version. ### Gradle — constraint merge, highest-wins Gradle imports each BOM as a **platform** contributing constraints. When two constraints target the same module, Gradle picks the **highest version that satisfies all constraints** during conflict resolution (unless you use `enforcedPlatform`, which forces and can conflict/fail). So Gradle is order-insensitive but 'highest-wins', which can silently move you past a BOM's tested version. ## Governing at organizational scale The strategic answer is a **single internal platform**: 1. **Company BOM / java-platform module.** Create one artifact (Maven `packaging=pom` with a `dependencyManagement`, or Gradle `java-platform` project). It **imports spring-boot-dependencies** for the chosen Boot version and adds your approved versions for internal libraries and any third-party libs not covered (or CVE-driven overrides). 2. **Every service imports only that platform** — `platform("com.acme:acme-platform:2025.3")` in Gradle or a single BOM import in Maven. This gives one upgrade lever, reproducibility, and consistency across dozens/hundreds of services. 3. **Controlled cadence.** The platform team bumps Boot + curates overrides on a schedule; teams pick up the whole tested set by bumping one platform version. 4. **Overrides policy.** Individual services should rarely override; when they must (urgent CVE), do it **family-level**, tracked in an issue, and removed at the next platform bump. 5. **Verification in CI.** Run `mvn dependency:tree`/`gradle dependencyInsight`, an SCA scanner (e.g. dependency-check/Snyk) for CVEs, and optionally a rule that forbids ad-hoc version pins outside the platform. ## Edge cases and gotchas - **Boot vs Spring Cloud alignment**: their release trains are paired; mismatched pairs are unsupported — the platform should encode the correct pairing. - **enforcedPlatform pitfalls**: forcing can downgrade a library another module needs, breaking it at runtime; prefer `platform` and resolve conflicts deliberately. - **Maven order bugs**: reordering imports can silently change resolved versions — treat import order as reviewed config, not incidental. - **Transitive BOM imports**: a BOM can itself import other BOMs; the effective managed set is the flattened union with first-wins per artifact — inspect the effective POM (`mvn help:effective-pom`). - **Reproducibility**: pin exact BOM versions (never version ranges) so builds are deterministic; consider Gradle dependency locking. ## When to invest A single service can just import the Boot BOM directly. The platform-module investment pays off once you have many services that must upgrade coherently and pass shared security/compliance gates.
- Two imported BOMs pin different jackson-databind versions in Maven — which is used?Maven uses first-declared-wins for BOM imports: the artifact's version comes from whichever import appears first in dependencyManagement. Order is significant; later imports of the same artifact are ignored.
- How does a java-platform module improve on each service importing spring-boot-dependencies directly?It centralizes the version matrix behind one artifact, so the whole org upgrades coherently by bumping one platform version, enforces approved/CVE-patched versions, and keeps Boot/Spring Cloud pairings correct — a single upgrade and governance lever.
saying these in an interview costs you the question
- Claiming Maven merges BOMs by highest-version (it's first-declared-wins).
- Assuming Gradle honors BOM import order like Maven (it merges constraints).
- Letting each service pin versions ad hoc instead of via a shared platform.
- Mixing mismatched Spring Boot and Spring Cloud release trains.