Maven Dependencies: Scope, Transitive Dependencies, Version Conflicts and BOM - Quiz
Total: 5 questions
1. What is Maven dependency scope, and how do compile, provided, runtime, test, system and import differ?
What is Maven dependency scope, and how do compile, provided, runtime, test, system and import differ?
Scope determines which classpaths Maven adds a dependency to (main compilation, test compilation and run, runtime) and whether it is passed on transitively to projects that depend on yours. The default is compile.
- compile - everywhere, including the war
WEB-INF/lib; transitive (Guava, Jackson). - provided - compilation and tests, but not runtime and not the war: the environment (servlet container, JDK) supplies the library; not transitive (
jakarta.servlet-api, Lombok). - runtime - not needed to compile the main code, present in tests, at runtime and in the war; transitive (JDBC driver, logging implementation).
- test - test compilation and run only; not transitive (JUnit, Mockito).
- system - deprecated: a local jar via an absolute
systemPath, the build is not portable. Install the jar into a local or corporate repository instead. - import - goes to no classpath; allowed only in
<dependencyManagement>together with<type>pom</type>and brings in versions from a BOM.
What ends up in the war is the behavior of maven-war-plugin; a regular jar built by maven-jar-plugin contains no dependencies at all. A wrong scope usually shows up at runtime, not at compile time. With provided scope on JDK 23+, Lombok is usually also listed in annotationProcessorPaths of maven-compiler-plugin: by default javac does not run annotation processors found on the classpath unless annotation processing is configured explicitly.
2. What are transitive dependencies in Maven, and how does scope affect whether they are passed on?
What are transitive dependencies in Maven, and how does scope affect whether they are passed on?
A transitive dependency is a dependency of your dependency: if your project uses A and the POM of A declares B, Maven adds B and its own dependencies too, at any depth. Their versions are chosen by the library authors, not by you.
The resulting scope, according to the Maven documentation, depends on the combination of the direct and transitive scopes:
- transitive
providedandtestdependencies do not get into the project; - with a direct
compiledependency: a transitive compile stays compile, runtime stays runtime; - with a direct
provided,runtimeortestdependency, transitive compile and runtime get the scope of the direct one. So a library that arrives through a test dependency is available to tests only.
optional is set by a library author on a dependency needed for some features only: consumers do not get it and add it themselves, otherwise a call to such a feature fails with NoClassDefFoundError.
It is easy to start using a transitive compile dependency by accident; the Maven documentation recommends declaring such dependencies directly. mvn dependency:analyze finds them (Used undeclared), while Unused declared lists declared but unused ones. The analysis is heuristic: it may flag libraries loaded via reflection, ServiceLoader or configuration, and compiler-only ones (Lombok), as unused.
3. How does Maven resolve a version conflict with the nearest wins rule, and how do you find and fix such a conflict?
How does Maven resolve a version conflict with the nearest wins rule, and how do you find and fix such a conflict?
A version conflict means the same library (same groupId and artifactId) appears in the tree in different versions. Maven keeps one by the nearest wins rule: the version closest to the project in the tree wins, and at equal depth the one encountered first in the declaration order. Whether a version is newer does not matter. A version declared directly or set in the project dependencyManagement beats transitive ones.
The build succeeds, and at runtime a library built for another version fails with NoSuchMethodError, NoClassDefFoundError/ClassNotFoundException or AbstractMethodError/IncompatibleClassChangeError.
How to find it: mvn dependency:tree shows the already resolved tree without the losing versions; -Dverbose adds the omitted nodes (support depends on the maven-dependency-plugin version), and -Dincludes=groupId:artifactId keeps only the paths to the library.
How to fix it: declare the required version directly (depth 1 wins), exclude the unwanted transitive dependency with <exclusions> (groupId and artifactId only, applies to one branch), or pin the version in <dependencyManagement> - preferred in multi-module projects. Reordering lines relies on luck; after forcing a newer version, verify the old library with tests.
4. How does dependencyManagement differ from dependencies, and what is a BOM in Maven?
How does dependencyManagement differ from dependencies, and what is a BOM in Maven?
<dependencyManagement> sets versions (and, if needed, scope and exclusions) but adds nothing: 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, which makes it the main way to close a version conflict. It usually lives in the parent POM: modules declare libraries without versions and get the same versions, while the dependencies themselves are not forced on them, unlike the parent <dependencies>. An explicit version in a project <dependency> takes priority over the managed one.
A BOM (Bill of Materials) is a POM with packaging = pom that contains only dependencyManagement with aligned versions of a set of libraries (junit-bom, spring-boot-dependencies, jackson-bom). You add it to your own dependencyManagement with <scope>import</scope> and <type>pom</type>; importing a BOM does not add a single library.
- If two imported BOMs manage the same library, the one declared first wins.
- Entries declared directly in your own dependencyManagement take priority over imported ones.
- BOM properties are resolved inside the BOM: on import, your
<properties>have no effect on them (unlike with the spring-boot-starter-parent parent), and the version silently stays the same.
5. What are the most common mistakes with Maven dependencies, and how does maven-enforcer-plugin help?
What are the most common mistakes with Maven dependencies, and how does maven-enforcer-plugin help?
Most of these mistakes do not break compilation: the problem shows up at runtime, on the server or in the projects that consume your library.
- provided without the environment: with plain
java -jarrather than a servlet container - ClassNotFoundException or NoClassDefFoundError. - Missing version:
'dependencies.dependency.version' for ... is missing- the library is not in dependencyManagement. - A SNAPSHOT dependency in a release: its contents change, and the build behaves differently a month later.
- The same classes in different artifacts:
javax.servlet:javax.servlet-api:4.0.1andjakarta.servlet:jakarta.servlet-api:4.0.4both contain thejavax.servletpackage. For Maven these are different libraries, so it sees no conflict and nearest wins does not apply: find the extra one with dependency:tree and exclude it manually in every branch. - Order instead of version: swapping two dependencies "fixes" the project - pin the version explicitly instead.
maven-enforcer-plugin stops the build when a rule is violated: dependencyConvergence - one library in the tree in different versions; requireUpperBoundDeps - nearest wins selected a version lower than a transitive dependency requires; requireReleaseDeps - bans SNAPSHOTs; bannedDependencies - bans specific artifacts. Duplicate classes are caught by banDuplicateClasses from extra-enforcer-rules (MojoHaus). The available rules and their parameters depend on the plugin version.