Nearest Wins Isn't Always Right: Fixing Maven's Silent Dependency Conflicts
Maven resolves version conflicts by tree depth, not by which version is newer. Diagnose with dependency:tree, pin with dependencyManagement, enforce with Enforcer.
17 Dec 2025, 15:40 UTC

Your build is green, the jar ships, and twenty minutes into production the stack trace says NoSuchMethodError for a method you can plainly see in the library's jar. The usual culprit isn't a missing jar — it's two versions of the same jar. Two of your dependencies each pull in the same transitive artifact at different versions, and Maven quietly resolved the conflict by picking one version for the whole build. The library that expected the other version fails the moment it runs.
Maven's conflict mediation is deterministic, but it's structural, not smart: it picks by tree depth, not by which version is newer or safer. That turns version conflicts into an engineering decision you have to make deliberately. The workflow that works: diagnose with dependency:tree, decide with dependencyManagement, and prevent with the Enforcer plugin.
How "nearest wins" actually decides
A transitive dependency is one you never declared yourself — it arrives because a library you did declare needs it. When the same artifact (same groupId and artifactId) appears on multiple paths of the dependency graph, Maven 3.x applies mediation: the definition closest to the root of the tree wins. If two candidates sit at the same depth, the first one declared in your POM wins.
Notice what's missing from that rule: any comparison of version numbers. Maven will happily resolve 2.15 over 2.17 because 2.15 happens to sit one level closer to your POM. Predictable, yes. Correct, not necessarily.
Reading the tree before you guess
mvn dependency:tree is the diagnostic. Run it from the module's directory on any machine with a JDK and Maven on PATH; it will download dependency POMs from your configured repositories, so it needs normal repository access. Filter to the suspect artifact so the output stays readable:
mvn dependency:tree -Dincludes=com.fasterxml.jackson.core:jackson-databind
For a project that declares lib-a and lib-b, both of which depend on jackson-databind at different versions, you should see something like this (versions illustrative; exact formatting varies slightly between dependency plugin releases):
[INFO] com.example:app:jar:1.0.0 [INFO] +- com.example:lib-a:jar:1.4.0:compile [INFO] | \- com.fasterxml.jackson.core:jackson-databind:jar:2.15.3:compile [INFO] \- com.example:lib-b:jar:2.1.0:compile [INFO] \- com.fasterxml.jackson.core:jackson-databind:jar:2.17.0:compile (omitted for conflict with 2.15.3)
The omitted for conflict annotation is the whole story in one line: lib-b asked for 2.17.0, but lib-a's request won because it sits nearer the root. Now you know which library is failing at runtime and which version it expected — an informed choice instead of bumping numbers at random.
Choosing where the version lives
The standard fix is dependencyManagement in your parent POM. A version declared there overrides transitive versions everywhere in the reactor, regardless of tree depth:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.17.0</version>
</dependency>
</dependencies>
</dependencyManagement>For a family of related artifacts, prefer importing a BOM (bill of materials — a POM that exists purely to publish coordinated versions) inside your dependencyManagement block:
<dependency> <groupId>com.fasterxml.jackson</groupId> <artifactId>jackson-bom</artifactId> <version>2.17.0</version> <type>pom</type> <scope>import</scope> </dependency>
Exclusions are the alternative: an <exclusions> block on lib-a cuts its path to jackson entirely. That's the scalpel — right for one known-bad path, but it scales poorly. dependencyManagement says "always this version"; an exclusion says "never via this path," and across several modules that becomes archaeology instead of policy.
The trade-off worth naming: centralizing versions concentrates maintenance. Every module must track the parent or BOM, and upgrades become a coordinated event. A pin also isn't a compatibility guarantee — it just makes the choice explicit. Run your test suite after changing one.
Turning runtime surprises into build failures
To stop the next conflict from landing silently, add the maven-enforcer-plugin with the dependencyConvergence rule, which fails the build when the same artifact is requested at two different versions. Its sibling requireUpperBoundDeps fails when the resolved version is lower than the highest version any path requested — useful when you pinned the older one on purpose and want that decision surfaced. Pin a current plugin version from the official documentation; Maven warns when one is missing.
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-enforcer-plugin</artifactId>
<executions>
<execution>
<goals><goal>enforce</goal></goals>
<configuration>
<rules>
<dependencyConvergence/>
</rules>
</configuration>
</execution>
</executions>
</plugin>One caveat: dependencyConvergence is noisy on large legacy trees with many conflicting paths. A practical adoption path is enabling it per-module or as a non-blocking CI report first, fixing the easy conflicts, then enforcing it globally.
Make the conflict visible early
Three habits close the loop. Run dependency:tree whenever you see a linkage error — NoSuchMethodError, AbstractMethodError, or a ClassNotFoundException for a class you know exists — instead of guessing. Pin shared versions in dependencyManagement or a BOM so the decision is written down once. Add a convergence rule to CI so the next conflict fails the build, not production. To verify a fix, re-run the tree: every path should resolve to your pinned version, and overridden nodes will be annotated as version-managed.
One forward-looking note: the mediation behavior described here is Maven 3.x. Maven 4 changes several build behaviors, so if you're migrating, confirm resolution semantics against the documentation for the version you actually run.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.