Maven Dependencies: Scope, Transitive Dependencies, Version Conflicts and BOM
A project uses two hypothetical libraries: report-lib relies on Guava 19.0 internally, and http-lib relies on Guava 33.4.0-jre. Both are declared in pom.xml, with report-lib first. mvn package succeeds, but the application crashes at runtime with java.lang.NoSuchMethodError: 'java.util.stream.Collector com.google.common.collect.ImmutableList.toImmutableList()'. Maven silently picked Guava 19.0: both versions sit at the same depth of the dependency tree, so the one declared first wins. And the toImmutableList() method that http-lib calls was only added in Guava 21. Swap the two dependencies and the error disappears — below we look at why this happens and how to control it explicitly.
A Maven dependency is an external artifact (usually a jar library) that your project uses and that is declared in pom.xml with a <dependency> element by its coordinates: groupId, artifactId and version. Maven finds it in a repository, adds it to the right classpaths depending on its scope, and pulls in its own, transitive dependencies. If the same library appears in the tree in several versions, Maven keeps only one of them according to the nearest wins rule, and you control that choice with <exclusions>, <dependencyManagement> and BOMs. What dependencies are and why you need them at all is briefly covered in the lesson What Is Maven; this lesson is about managing them. The examples target Maven 3.9.x and Java 25.
How to Declare a Dependency: GAV, type, classifier, optional
A Maven dependency is declared with a <dependency> element inside <dependencies>: groupId and artifactId are required, and version is required unless the version is set in dependencyManagement (your own, the parent's, or one imported from a BOM). The other elements — scope, type, classifier, optional, exclusions — are optional and specify exactly which file you need and how it is used.
<dependencies>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>33.4.0-jre</version>
<!-- scope is omitted: compile by default -->
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-transport-native-epoll</artifactId>
<version>4.1.115.Final</version>
<!-- native library for one platform -->
<classifier>linux-x86_64</classifier>
</dependency>
</dependencies> | Element | What it defines | Default | Example value |
|---|---|---|---|
groupId, artifactId, version | Library coordinates (GAV) | version — from dependencyManagement, otherwise an error | com.google.guava, guava, 33.4.0-jre |
scope | Which classpaths the dependency goes to and whether it is passed on | compile | test, provided, runtime |
type | Artifact type (determines the file extension) | jar | pom (for a BOM), test-jar, war |
classifier | A variant of the artifact with the same GAV | None (the main artifact) | linux-x86_64, sources, tests |
optional | Do not pass the dependency on to consumers of your project | false | true |
exclusions | Transitive dependencies to drop | None | See the exclusions section |
Where to find the coordinates. The official Maven Central search is central.sonatype.com (formerly search.maven.org). An artifact page gives you a ready-to-paste <dependency> snippet and the list of all versions. The unofficial catalog mvnrepository.com is handy because it shows which dependencies a library pulls in and which versions have known vulnerabilities. Copy coordinates from there rather than from old articles: many libraries have changed their groupId (for example, when moving from javax.* to jakarta.*). The version numbers in this lesson are real, but always check the latest version on Maven Central. Where Maven downloads files from and where it stores them locally is covered in a separate lesson of this module on Maven repositories.
Maven Dependency Scope: compile, provided, runtime, test, system, import
The scope of a Maven dependency determines which classpaths Maven adds it to — for compiling the main code, for compiling and running tests, for running the application — and whether it is passed on transitively to projects that depend on yours. The default is compile. A wrong scope usually shows up not at compile time but at runtime: a ClassNotFoundException or unwanted libraries in the build.
| Scope | Main compilation | Test compilation and run | Runtime classpath | In war WEB-INF/lib | Transitive | Typical example |
|---|---|---|---|---|---|---|
compile (default) | Yes | Yes | Yes | Yes | Yes | Guava, Jackson, commons-lang3 |
provided | Yes | Yes | No — provided by the environment (server, JDK) | No | No | jakarta.servlet-api, Lombok |
runtime | No | Yes | Yes | Yes | Yes | PostgreSQL JDBC driver, logging implementation |
test | No | Yes | No | No | No | JUnit, Mockito, AssertJ |
system (deprecated) | Yes | Yes | No | No | No | A local jar via systemPath — do not use |
import | Goes to no classpath at all: allowed only in <dependencyManagement> together with <type>pom</type> and brings in the versions from a BOM | junit-bom, spring-boot-dependencies | ||||
The "In war WEB-INF/lib" column describes the behavior of maven-war-plugin. A regular jar built by maven-jar-plugin contains no dependencies at all, whatever their scope. Which dependencies end up in a "fat" jar is decided by the plugin that builds it (for example, maven-shade-plugin or spring-boot-maven-plugin) — check its documentation.
<dependencies>
<!-- the servlet container (Tomcat) provides it at runtime -->
<dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
<version>6.1.0</version>
<scope>provided</scope>
</dependency>
<!-- code uses only java.sql.*, the driver is loaded at runtime -->
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.7.4</version>
<scope>runtime</scope>
</dependency>
<!-- tests only -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.11.4</version>
<scope>test</scope>
</dependency>
</dependencies> A JDBC driver is declared with runtime scope because the code works only with the interfaces java.sql.Connection and DriverManager, and the actual driver is found at runtime. If you accidentally import a driver class in your code, for example org.postgresql.PGConnection, compilation fails with package org.postgresql does not exist — a useful safeguard against tying the code to one database.
The system scope requires an absolute path to the jar in <systemPath> and is marked as deprecated in the Maven documentation: the build is no longer portable, and consumers of the project do not get that file. If a jar is not available in public repositories, install it into your local or corporate repository and use it as a regular dependency.
Lombok: provided does not mean it works
Lombok is needed only at compile time, so it is declared with provided scope (JDK 25 support arrived in Lombok 1.18.40; Lombok supports new JDKs with a delay, so check the changelog and use a recent version). But starting with JDK 23, javac no longer runs annotation processors found on the classpath by default unless annotation processing is configured explicitly. Then @Getter "does nothing" and compilation fails with cannot find symbol on the getter call. The usual fix is to also list Lombok in annotationProcessorPaths of maven-compiler-plugin; plugin configuration is covered in a separate lesson of this module.
Maven Transitive Dependencies
A transitive dependency is a dependency of your dependency: if your project uses library A and the POM of A declares library B, Maven adds B and its own dependencies to your project too, at any depth. Whether a transitive dependency ends up in the project, and with which scope, depends on the combination of the direct and transitive scopes; optional dependencies and dependencies with provided or test scope are not passed on.
For example, a single junit-jupiter entry in pom.xml brings several more artifacts into the project: the Jupiter API, the engine, junit-platform-commons, opentest4j and others. You do not have to declare them by hand — but their versions are chosen by the library authors, not by you.
The resulting scope of a transitive dependency according to the Maven documentation (row — the scope of your direct dependency, column — the scope the transitive dependency is declared with in its POM; "—" means the dependency does not get into the project):
| Direct \ transitive | compile | provided | runtime | test |
|---|---|---|---|---|
compile | compile | — | runtime | — |
provided | provided | — | provided | — |
runtime | runtime | — | runtime | — |
test | test | — | test | — |
Two practical conclusions follow from the table. First: a library that arrives transitively through a test dependency is available to tests only, even if its own POM declares it as compile. Second: a transitive compile dependency is visible when your code compiles, so it is easy to start using it "by accident". The Maven documentation explicitly recommends declaring such dependencies directly: if the library author removes it in the next version, your code stops compiling. mvn dependency:analyze helps you find such places.
optional. The library author puts <optional>true</optional> on a dependency needed only for some features. The library itself compiles against it, but consumer projects do not get it and must add it explicitly if they use that feature:
<!-- in the POM of a library, for example report-lib -->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>5.3.0</version>
<!-- needed only for Excel export; users add it themselves -->
<optional>true</optional>
</dependency> If a consumer calls the export feature without adding poi-ooxml, the build passes, but the call fails at runtime with NoClassDefFoundError. optional is a tool for library authors; in a regular application that nobody uses as a dependency, the flag changes nothing.
Maven Dependency Conflicts: the Nearest Wins Rule
A Maven dependency conflict occurs when the same library (same groupId and artifactId) appears in the dependency tree in different versions. Maven keeps exactly one version on the classpath according to the nearest wins rule (dependency mediation): the version closest to your project in the tree wins; at equal depth, the one encountered first in the declaration order wins. Whether a version is newer does not matter.
com.examclouds:shop
+- com.example:report-lib:1.0.0
| \- com.google.guava:guava:19.0 depth 2, declared first -> WINS
\- com.example:http-lib:2.0.0
\- com.google.guava:guava:33.4.0-jre depth 2, declared second -> omitted | Situation in the tree | Version Maven picks |
|---|---|
The version is declared directly in your pom.xml (depth 1) | That one, whatever versions arrive transitively |
The version is set in your project's dependencyManagement (own, parent's or from a BOM) | That one — a managed version also applies to transitive dependencies |
| Transitive versions at different depths | The one closer to the root of the tree |
| Transitive versions at the same depth | The one that comes first in the order of <dependency> declarations |
The danger of this rule is that the build succeeds. Your code compiles against the selected version, and a library built for a different version breaks only when execution reaches the missing API:
NoSuchMethodError— the class exists, but the selected version lacks the required method (or its signature); as in the example at the start of the lesson.NoClassDefFoundErrororClassNotFoundException— the selected version lacks an entire class (it was added later or moved to another package).AbstractMethodError,IncompatibleClassChangeError— the hierarchy or an interface has changed.
So the order of <dependency> elements in pom.xml can affect the result, and "fixing" a conflict by reordering lines is risky: it relies on luck and breaks with the next upgrade. The reliable fix is to pin the required version explicitly (see below). The opposite can happen too: if you force a newer version, an old library may not find an API removed in that version, so verify it with tests after upgrading.
mvn dependency:tree and dependency:analyze
The mvn dependency:tree command prints the project's dependency tree with the versions and scopes Maven selected after resolving conflicts, and mvn dependency:analyze compares the declared dependencies with the ones actually used in the compiled classes. Both are goals of maven-dependency-plugin, which you do not need to declare in pom.xml to run them from the command line.
The dependency tree
mvn dependency:tree Output for the project from the start of the lesson (a fragment; the exact formatting depends on the plugin version):
[INFO] com.examclouds:shop:jar:1.0-SNAPSHOT
[INFO] +- com.example:report-lib:jar:1.0.0:compile
[INFO] | \- com.google.guava:guava:jar:19.0:compile
[INFO] +- com.example:http-lib:jar:2.0.0:compile
[INFO] \- org.junit.jupiter:junit-jupiter:jar:5.11.4:test
[INFO] +- org.junit.jupiter:junit-jupiter-api:jar:5.11.4:test
[INFO] ... Notation: +- — another dependency at this level, \- — the last one at this level, | — a branch continues. The line format is groupId:artifactId:type:version:scope (a classifier, if present, goes before the version).
Edge case: the losing version is invisible in the tree
A plain dependency:tree shows the already resolved tree. In the output above, http-lib has no child lines at all: the Guava 33.4.0-jre it asked for was dropped, and nothing in the tree tells you there was a conflict. To see the omitted versions, you need -Dverbose.
-Dverbose adds the omitted nodes to the tree together with the reason. Support for this option depends on the maven-dependency-plugin version: it did not work in early 3.x releases and was restored later (in 3.2.0), so if the result looks odd, specify a recent plugin version explicitly, for example mvn org.apache.maven.plugins:maven-dependency-plugin:3.8.1:tree -Dverbose. The lines look approximately like this:
mvn dependency:tree -Dverbose
[INFO] +- com.example:report-lib:jar:1.0.0:compile
[INFO] | \- com.google.guava:guava:jar:19.0:compile
[INFO] +- com.example:http-lib:jar:2.0.0:compile
[INFO] | \- (com.google.guava:guava:jar:33.4.0-jre:compile - omitted for conflict with 19.0) -Dincludes keeps only the paths to the library you are interested in — on a large project it is the only quick way to answer "who pulled in this version?". The filter format is groupId:artifactId, * wildcards are allowed, and the filter can be combined with -Dverbose:
mvn dependency:tree -Dincludes=com.google.guava:guava [INFO] com.examclouds:shop:jar:1.0-SNAPSHOT
[INFO] \- com.example:report-lib:jar:1.0.0:compile
[INFO] \- com.google.guava:guava:jar:19.0:compile In IntelliJ IDEA the same tree is shown under the Dependencies node of the Maven tool window, and the Ultimate edition has a separate dependency diagram that highlights conflicts.
Analyzing used dependencies
mvn dependency:analyze inspects the bytecode of the compiled classes and prints two lists: Used undeclared — the code uses classes from a library that arrived only transitively (it should be declared explicitly); Unused declared — a dependency is declared, but none of its classes appear in the code (a candidate for removal or a scope change).
mvn dependency:analyze
[WARNING] Used undeclared dependencies found:
[WARNING] com.google.guava:guava:jar:19.0:compile
[WARNING] Unused declared dependencies found:
[WARNING] org.apache.commons:commons-lang3:jar:3.17.0:compile The analysis is heuristic: it does not see libraries loaded via reflection, ServiceLoader or configuration, and it knows nothing about libraries needed only by the compiler (Lombok). It may flag such dependencies as unused — check the build and the application before removing them.
How to Resolve a Version Conflict: Explicit Version and Exclusions
A Maven dependency conflict is resolved in one of three ways: declare the required version of the library directly in your pom.xml (depth 1 always wins), exclude the unwanted transitive dependency with <exclusions>, or pin the version in <dependencyManagement>. For one or two libraries in a small project any of them works; in multi-module projects and for sets of related libraries dependencyManagement is preferred.
| Approach | What it does | When it fits | Drawback |
|---|---|---|---|
| Direct dependency with the required version | The library moves to depth 1 and wins by nearest wins | Your code already uses this library | Adds a dependency your code may not use; dependency:analyze will report it as unused |
<exclusions> | Removes a transitive dependency from one specific branch of the tree | The library is not needed at all or is replaced by another one (a logging bridge) | Works on one branch only; another library may bring the same artifact back |
<dependencyManagement> | Pins the version for every occurrence of the library in the tree without adding it | Almost always, especially in multi-module projects | The version has to be maintained by hand (or via a BOM) |
An explicit version for the example from the start of the lesson:
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>33.4.0-jre</version>
</dependency> After that, mvn dependency:tree -Dincludes=com.google.guava:guava shows Guava directly under the project root with version 33.4.0-jre, and the NoSuchMethodError is gone. Now it is report-lib that runs on a newer Guava than the one it was built against — run the tests that exercise its code: Guava removes deprecated APIs between major versions.
Maven exclusions — excluding a transitive dependency. A classic example: org.apache.httpcomponents:httpclient 4.5.x pulls in commons-logging, while the project logs through SLF4J. You exclude commons-logging and add the jcl-over-slf4j bridge instead, which provides the same org.apache.commons.logging.* classes but routes the calls to SLF4J:
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.14</version>
<exclusions>
<exclusion>
<!-- version is not specified: all versions are excluded -->
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>jcl-over-slf4j</artifactId>
<version>2.0.16</version>
</dependency> An <exclusion> contains only groupId and artifactId, without a version. The exclusion applies only to the branch of the tree under the dependency where it is written: if commons-logging also arrives through another library, you have to exclude it there as well (check with dependency:tree -Dincludes=commons-logging:commons-logging). If you exclude a dependency without a replacement, expect a NoClassDefFoundError the first time the library touches its classes.
dependencyManagement: Versions in One Place
The <dependencyManagement> section sets versions (and, if needed, scope and exclusions) for dependencies but adds nothing by itself: a library gets into the project only if it is declared in the regular <dependencies> section — then without <version>. A managed version also applies to transitive occurrences of the library, which makes dependencyManagement the main way to close a version conflict for good.
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>33.4.0-jre</version>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!-- guava is NOT declared here, but report-lib still gets 33.4.0-jre -->
<dependency>
<groupId>com.example</groupId>
<artifactId>report-lib</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>com.example</groupId>
<artifactId>http-lib</artifactId>
<version>2.0.0</version>
</dependency>
</dependencies> Now dependency:tree shows Guava under report-lib with version 33.4.0-jre, and in -Dverbose mode the node usually gets a note like version managed from 19.0. If you declare a library that is not in dependencyManagement without a version, the build stops as soon as the model is read:
[ERROR] 'dependencies.dependency.version' for com.google.code.gson:gson:jar is missing. Inheritance. Most often dependencyManagement lives in the parent POM: child modules declare the libraries they need without versions, and all modules are guaranteed to use the same versions. The dependencies themselves are not forced on the modules, unlike the parent's <dependencies>, which every child inherits. Multi-module projects will be covered in a separate lesson; how <parent> works and where to see the final managed versions (mvn help:effective-pom) is explained in the section parent, Super POM and Effective POM. The version numbers themselves are best moved to Maven properties.
An explicit version in a project's <dependency> takes priority over the managed one. So if a managed version "is not applied", first check whether the version is written directly in the module's dependency.
Maven BOM: Importing a Set of Versions
A BOM (Bill of Materials) is a POM file with packaging = pom that contains only a <dependencyManagement> section with aligned versions of a set of related libraries. You add it to your own <dependencyManagement> with <scope>import</scope> and <type>pom</type>: all managed versions from the BOM become yours, and the libraries themselves are then declared without versions.
An example with junit-bom: the version is specified once, and the JUnit modules (junit-jupiter, junit-platform-suite and others) are guaranteed to get compatible versions.
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.junit</groupId>
<artifactId>junit-bom</artifactId>
<version>5.11.4</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<!-- version comes from junit-bom -->
<scope>test</scope>
</dependency>
</dependencies> Many ecosystems manage versions with their own BOMs: Spring Boot (org.springframework.boot:spring-boot-dependencies), Jackson (com.fasterxml.jackson:jackson-bom), Netty, AWS SDK. In Spring Boot projects the same versions usually come from the spring-boot-starter-parent parent, and the BOM import is used when the parent slot is already taken (for example, by a corporate POM):
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<!-- check the latest version on Maven Central -->
<version>3.5.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement> Order of several BOMs. If two imported BOMs manage the same library, the one declared first in <dependencyManagement> wins. Entries declared directly in your own dependencyManagement take priority over imported ones. Hence the rule: to bump the version of one library from a BOM, declare it explicitly in dependencyManagement or put that library's BOM (for example, jackson-bom) above the framework's general BOM.
Tricky case: BOM properties are not overridden on import
With the spring-boot-starter-parent parent, you change, say, the Jackson version with a single property in your own <properties> (the property name is in the Spring Boot documentation). When you import spring-boot-dependencies with scope = import, this trick does not work: the BOM's properties are resolved inside the BOM itself, and your <properties> have no effect on them. The version silently stays the same. With an import, override the version with a dependencyManagement entry (or the library's BOM) declared before the general BOM import.
Where Developers Go Wrong with Dependencies
Most problems with Maven dependencies do not break compilation: the build passes, and the error shows up at runtime, on the server or in the projects that consume your library. The table lists the symptom, the cause and the fix; for continuous control in CI, use maven-enforcer-plugin.
| What happened | Cause | How to fix |
|---|---|---|
'dependencies.dependency.version' for ... is missing | The version is not specified, and the library is not in dependencyManagement (own, parent's, imported BOM) | Specify <version> or add the library or its BOM to dependencyManagement |
ClassNotFoundException or NoClassDefFoundError at runtime, although everything compiles | The dependency is declared as provided, but the runtime environment does not provide it (plain java -jar, not a servlet container) | Change the scope to compile or runtime; keep provided only for what the server or JDK definitely supplies |
package org.junit.jupiter.api does not exist (or org.mockito) on mvn compile | A class from a test dependency is used in src/main/java | Move the code to src/test/java; if the main code really needs the library, remove scope = test |
NoSuchMethodError inside a library after adding a new dependency | The new dependency changed the version chosen by nearest wins | dependency:tree -Dverbose -Dincludes=..., then pin the version in dependencyManagement |
| A release build behaves differently a month after the release | A -SNAPSHOT dependency was left in the release: its contents change | Replace SNAPSHOTs with release versions before releasing; the enforcer rule requireReleaseDeps bans them automatically |
Duplicate classes, a "random" implementation being picked, ClassCastException between "identical" classes | The same classes arrived in two artifacts with different groupId/artifactId. For example, javax.servlet:javax.servlet-api:4.0.1 and jakarta.servlet:jakarta.servlet-api:4.0.4 both contain the javax.servlet package. For Maven these are different libraries, so it sees no conflict and nearest wins does not apply | Find both with dependency:tree and exclude the extra one manually in every branch with <exclusions>; duplicate classes are caught by the banDuplicateClasses rule from extra-enforcer-rules (MojoHaus) |
Swapping two <dependency> elements "fixes" or "breaks" the project | A conflict at the same depth is resolved by declaration order | Pin the version explicitly instead of relying on the order |
The code compiles against classes from a library that is not in pom.xml | A transitive compile dependency is being used | Declare it explicitly (dependency:analyze lists it under Used undeclared) |
Maven Enforcer. maven-enforcer-plugin stops the build when a configured rule is violated. Useful rules for dependencies: dependencyConvergence — fails if one library appears in the tree in different versions (forces you to resolve every conflict explicitly); requireUpperBoundDeps — fails if nearest wins selected a version lower than the one some transitive dependency requires (exactly the case from the start of the lesson); requireReleaseDeps — bans SNAPSHOT dependencies; bannedDependencies — bans specific artifacts. The available rules and their parameters depend on the plugin version. How to add plugins and bind them to phases is covered in separate lessons of this module on Maven plugins and the build lifecycle.
Frequently Asked Questions
What is the difference between provided scope and optional?
They answer different questions. provided says the environment (a servlet container, the JDK) will supply the library at runtime, so it is not on the runtime classpath or in WEB-INF/lib. optional says the dependency is needed only for some features of a library, so it is not passed on to consumer projects, while inside the library itself it behaves according to its scope. They have one thing in common: neither becomes transitive. More details are in the sections Maven Dependency Scope and Maven Transitive Dependencies.
How do I exclude all transitive dependencies of a library at once?
Since Maven 3.2.1 you can put * in both groupId and artifactId of an <exclusion> — the library is then added without any of its dependencies. You rarely need this: everything the library actually uses has to be declared by you, otherwise you get a NoClassDefFoundError at runtime.
Can I use a version range instead of an exact version?
Yes: Maven understands ranges such as [1.0,2.0) (from 1.0 inclusive to 2.0 exclusive). But then the build result depends on what is published in the repository at build time, and today's build may differ from yesterday's. For reproducible builds use exact versions and update them deliberately — for example, with versions-maven-plugin (the versions:display-dependency-updates goal lists available updates).
Why didn't a dependency from dependencyManagement show up in the project?
Because dependencyManagement only sets a version "in case the library is used" and adds nothing. To put the library on the classpath, declare it in the regular <dependencies> section — without <version>. The same goes for a BOM: importing a BOM does not add a single library. See the example in the section dependencyManagement.
How do I find out which library pulled in an unwanted dependency?
Run mvn dependency:tree -Dincludes=groupId:artifactId with the coordinates of the unwanted library: the output keeps only the paths from your project to it, and the first node of each path is your direct dependency, inside which you put the <exclusion>. If there are several paths, you have to exclude it in each one. An output example is in the section mvn dependency:tree.
Comments