Maven Dependencies & Repositories
Maven Dependencies & Repositories
Declaring a dependency in a pom.xml is trivial — one XML block and Maven resolves, downloads, and wires everything. The sophistication lies in understanding what Maven fetches on your behalf, how it decides which version to use when two libraries disagree, and why the wrong scope can silently bloat a production artifact or break a downstream consumer. This lesson turns that background knowledge into deliberate, professional dependency management.
How Maven Resolves Dependencies
When you declare a dependency, Maven searches a chain of repositories in order until it finds the artifact:
- Local repository —
~/.m2/repositoryon your machine. Once downloaded, artifacts are cached here forever (until you delete them or runmvn dependency:purge-local-repository). - Central — Maven Central, the public default. No configuration required.
- Additional remote repositories — declared in
pom.xmlorsettings.xml(e.g. JitPack, Spring Milestones, your company Nexus/Artifactory).
A coordinate — groupId:artifactId:version — uniquely identifies an artifact. Maven maps it to a filesystem path in any repository: com/google/guava/guava/32.1.3-jre/guava-32.1.3-jre.jar.
Dependency Scopes
The <scope> element controls three things simultaneously: which classpath the dependency appears on (compile / test / runtime), whether it is included in the final artifact, and whether it propagates to consumers of your library. The six scopes are:
- compile (default) — Available at compile time, runtime, and test time. Included in the packaged JAR/WAR and propagated to consumers as a transitive dependency. Use for APIs your code exposes or types that appear in your public signatures.
- provided — Available at compile and test time but not packaged and not transitive. The runtime environment (container, JDK) supplies it. Classic example:
jakarta.servlet-apiin a WAR deployed to Tomcat. - runtime — Not needed to compile but required at runtime. Packaged, but excluded from the compile classpath so your code cannot accidentally depend on implementation details. JDBC drivers belong here.
- test — Only the test compile and test runtime classpaths. Never packaged or propagated. JUnit, Mockito, AssertJ, Testcontainers.
- system — Like
providedbut you supply an explicit path via<systemPath>. Avoid: it is non-portable and essentially deprecated. - import — Only valid in
<dependencyManagement>withtype=pom. Merges a Bill of Materials (BOM) into your dependency management. Covered in Lesson 9.
provided scope (and document the requirement), consumers choose their own SLF4J binding. Scope is part of your library's contract.
Transitive Dependencies
When you depend on spring-webmvc, it in turn depends on spring-core, spring-beans, and others. Maven reads each dependency's POM and recursively resolves the full graph. This is transitive dependency resolution — you declare one artifact and Maven silently pulls a tree of artifacts.
Scope propagates through the graph with these rules: a transitive dependency with compile scope stays compile; one with runtime scope stays runtime; one with test or provided scope is not propagated at all. This is why you never see JUnit leak into production classpaths.
To inspect the full resolved graph:
Version Conflict Resolution: Nearest-Wins
The graph almost always contains the same artifact at multiple versions (diamond dependency). Maven applies the nearest-wins rule: the version declared closest to your project root in the dependency graph wins. If two paths have the same depth, the first one declared in the POM wins.
The nearest-wins rule is deterministic but can surprise you. If library A needs Jackson 2.17 and library B needs Jackson 2.15, and B is declared first at depth 2 while A pulls Jackson at depth 3, you get 2.15 — possibly incompatible with A's API calls.
mvn dependency:tree and mvn dependency:analyze regularly. When a transitive version is wrong, override it explicitly in <dependencyManagement>: declare the artifact with the version you need and Maven pins every reference in the tree to that version, regardless of depth.
Excluding Unwanted Transitives
Sometimes a library drags in a transitive dependency you do not want — a competing logging implementation, a vulnerable old version you cannot override globally, or an artifact that conflicts with your runtime environment. Use <exclusions> to cut it from the graph at the declaration site:
Adding a Remote Repository
Some artifacts are not on Maven Central (Spring milestones, JitPack builds from GitHub, your company's private repo). Declare them in the POM or in ~/.m2/settings.xml:
<mirrorOf> in settings.xml to route all traffic through your company proxy (Nexus / Artifactory), which can scan artifacts before they reach developer machines.
Practical Diagnostic Commands
mvn dependency:tree— full resolved graph with scopes and version sources.mvn dependency:analyze— flags used-but-undeclared and declared-but-unused dependencies.mvn dependency:resolve -Dclassifier=sources— download sources JARs for IDE navigation.mvn versions:display-dependency-updates— show which declared dependencies have newer releases.
Summary
Dependency management in Maven is a three-layer discipline: scopes constrain when and where a dependency is visible; transitive resolution automates the dependency graph but introduces version conflicts; repositories determine where Maven looks for artifacts. Mastering dependency:tree, understanding nearest-wins, and using <dependencyManagement> to pin versions are the skills that separate a developer who pastes coordinates from one who truly owns their build.