skip to content

How does Gradle's default conflict resolution differ from Maven's, and why does it matter?

level: seniorimportance: should knowfreq 48%

answer

  1. Maven = nearest definition wins
  2. Gradle = highest version wins
  3. depth/declaration order vs version ordering
  4. migration can change resolved versions
  5. hoisting doesn't pin in Gradle

basics

~10 s

Maven uses 'nearest definition wins' — the version closest to the root of the tree. Gradle uses 'highest version wins' regardless of depth. So migrating builds can silently pick different versions.

solid answer

~50 s

Maven resolves conflicts by **nearest-wins**: the version declared at the shallowest depth in the dependency tree (closest to the project root, ties broken by declaration order) is selected, even if a deeper node requests a newer version. Gradle instead uses **highest-version-wins**: among all requested versions of a module it picks the highest regardless of where it sits in the graph. This matters during Maven→Gradle migration because the **same POMs can resolve to different versions** — a deep transitive can now 'win' in Gradle when it would have been overridden in Maven. The practical consequences: (1) Gradle tends to pull newer (sometimes breaking) versions; (2) you can't rely on 'declare it near the top to pin it' — that's a Maven trick. To pin in Gradle you use constraints, platforms, forcing, or `failOnVersionConflict()` to make drift explicit. Both systems agree only when the highest-requested version also happens to be the nearest one.

code

bash · 4 lines
bash
# After a Maven -> Gradle migration, diff resolved versions:
mvn dependency:tree            # nearest-wins view
./gradlew dependencies         # highest-wins view
# Look for modules whose chosen version differs between the two.

go deeper

for a junior

Know the one-liner: Maven nearest-wins, Gradle highest-wins.

for a middle

Explain depth/declaration-order vs version ordering and that hoisting doesn't pin in Gradle.

for a senior

Discuss migration impact, how to diff resolved versions, and the right Gradle pinning mechanisms.

for a principal

Set migration governance: mandate version diffing and a platform/BOM strategy so resolution is deterministic across the org.

## Two different rules A conflict = same module requested at multiple versions. The two build tools resolve it differently: ### Maven — nearest definition wins Maven walks the dependency tree and, for a given module, picks the version found at the **least depth** from the project root. If A (depth 1) brings `lib:1.0` and B->C (depth 2) brings `lib:2.0`, Maven keeps **1.0** because it's nearer. Ties at equal depth are broken by **declaration order** in the POM (first wins). This means a top-level declaration effectively pins the version. ### Gradle — highest version wins Gradle ignores depth. For each module it collects every requested version across the whole graph and selects the **highest** by version ordering. The same A/B/C example resolves to **2.0** in Gradle. ## Why it matters 1. **Migration surprises.** The identical set of POMs can produce *different* classpaths. A build that worked on Maven may pull a newer, possibly breaking version under Gradle. Always diff resolved versions after migrating (`./gradlew dependencies` vs `mvn dependency:tree`). 2. **Different pinning idioms.** In Maven you 'hoist' a dependency to the top (or use `dependencyManagement`) to pin it. In Gradle, hoisting does **not** pin — a deeper transitive can still win if it's higher. You must use real version control: dependency constraints, a platform/BOM, or forcing. 3. **Newer-by-default risk.** Gradle's bias toward the highest version is great for getting security fixes but risks silently absorbing breaking majors; that's why `failOnVersionConflict()` exists. ## When they agree They produce the same result whenever the **nearest** version is also the **highest** — common in simple graphs, which is why many migrations 'just work' and the difference is overlooked until a regression appears. ## Practical guidance - After migration, compare resolved versions, not just whether the build compiles. - Treat top-of-tree declaration as documentation, not as a pin, in Gradle. - If you need Maven-like determinism, drive versions from a platform/BOM and/or enable conflict-failure. ```text Maven (nearest) Gradle (highest) A -> lib:1.0 (depth 1) WINS lib:1.0 B -> C -> lib:2.0 (depth 2) lib:2.0 WINS Result: 1.0 2.0 ```

  • In Maven, how do ties at equal depth break?
    By declaration order in the POM — the first declared wins. Gradle has no equivalent because it doesn't use depth at all; it compares versions.
  • Does declaring a dependency at the top of the build script pin its version in Gradle?
    No. Unlike Maven's nearest-wins, top-level declaration doesn't override a higher transitive. You pin with constraints, a platform/BOM, or forcing.
  • When do the two strategies produce the same classpath?
    Whenever the nearest version is also the highest. Simple graphs often satisfy this, masking the difference until a deep transitive bumps higher.

Maven asks 'who's standing closest to me?'; Gradle asks 'who's the most senior in the room?' — different people can win the same argument.

saying these in an interview costs you the question

  • Saying Gradle uses nearest-wins like Maven.
  • Assuming a Maven->Gradle migration can't change resolved versions.
  • Claiming top-level declaration pins a version in Gradle.

context