Maven Dependency Management
Dependencies are the libraries your project needs. Maven downloads them automatically, resolves their transitive dependencies, and puts them on the classpath. You just declare what you need.
Declaring Dependencies
<dependencies>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>33.2.1-jre</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.10.3</version>
<scope>test</scope>
</dependency>
</dependencies>
Dependency Scopes
| Scope | Compile Classpath | Test Classpath | Runtime Classpath | Packaged in JAR? | Use Case |
|---|---|---|---|---|---|
| compile (default) | ✅ | ✅ | ✅ | ✅ | Most libraries |
| provided | ✅ | ✅ | ❌ | ❌ | Servlet API (server provides it) |
| runtime | ❌ | ✅ | ✅ | ✅ | JDBC drivers |
| test | ❌ | ✅ | ❌ | ❌ | JUnit, Mockito |
| system | ✅ | ✅ | ❌ | ❌ | Local JAR (avoid this) |
<!-- compile scope (default — no need to specify) -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.17.1</version>
</dependency>
<!-- provided — container supplies it -->
<dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
<version>6.1.0</version>
<scope>provided</scope>
</dependency>
<!-- runtime — not needed at compile time -->
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.7.3</version>
<scope>runtime</scope>
</dependency>
<!-- test — only for testing -->
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>5.12.0</version>
<scope>test</scope>
</dependency>
Transitive Dependencies
When you add spring-boot-starter-web, Maven also downloads its dependencies (and their dependencies):
spring-boot-starter-web
├── spring-boot-starter
│ ├── spring-boot
│ ├── spring-boot-autoconfigure
│ └── spring-core
├── spring-web
├── spring-webmvc
├── spring-boot-starter-tomcat
│ └── tomcat-embed-core
└── jackson-databind
├── jackson-core
└── jackson-annotations
Dependency Tree
mvn dependency:tree
Output:
[INFO] com.myapp:my-project:jar:1.0.0
[INFO] +- org.springframework.boot:spring-boot-starter-web:jar:3.3.0:compile
[INFO] | +- org.springframework.boot:spring-boot-starter:jar:3.3.0:compile
[INFO] | +- org.springframework:spring-web:jar:6.1.8:compile
[INFO] | +- org.springframework:spring-webmvc:jar:6.1.8:compile
[INFO] | \- com.fasterxml.jackson.core:jackson-databind:jar:2.17.1:compile
[INFO] +- org.junit.jupiter:junit-jupiter:jar:5.10.3:test
Filter for a specific dependency:
mvn dependency:tree -Dincludes=com.fasterxml.jackson.core
Excluding Transitive Dependencies
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>3.3.0</version>
<exclusions>
<!-- Exclude Tomcat, use Jetty instead -->
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- Replace with Jetty -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jetty</artifactId>
<version>3.3.0</version>
</dependency>
BOM — Bill of Materials
A BOM manages versions centrally. You declare dependencies without versions:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>3.3.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!-- No version needed — BOM provides it -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
</dependencies>
Maven Central
The default repository — where Maven downloads dependencies from:
Search: search.maven.org
<!-- This is implicit (from Super POM) — no need to declare -->
<repositories>
<repository>
<id>central</id>
<url>https://repo.maven.apache.org/maven2</url>
</repository>
</repositories>
Private Repositories (Nexus/Artifactory)
<repositories>
<repository>
<id>company-repo</id>
<url>https://nexus.mycompany.com/repository/maven-releases/</url>
</repository>
</repositories>
Credentials go in ~/.m2/settings.xml (never in pom.xml):
<settings>
<servers>
<server>
<id>company-repo</id>
<username>deploy-user</username>
<password>secret</password>
</server>
</servers>
</settings>
SNAPSHOT Versions
<version>1.0.0-SNAPSHOT</version>
| Release | SNAPSHOT |
|---|---|
| Immutable | Changes with every build |
| Cached by Maven | Re-downloaded periodically |
| Production-ready | Development/in-progress |
1.0.0 |
1.0.0-SNAPSHOT |
# Force re-download SNAPSHOTs
mvn clean package -U
Local Repository
Maven caches all downloaded dependencies in:
~/.m2/repository/
├── com/
│ └── google/
│ └── guava/
│ └── guava/
│ └── 33.2.1-jre/
│ ├── guava-33.2.1-jre.jar
│ └── guava-33.2.1-jre.pom
# Clear local cache (re-downloads everything)
rm -rf ~/.m2/repository
Common Mistakes
| Mistake | Fix |
|---|---|
Missing <scope>test</scope> on JUnit |
Test dependencies in production JAR |
| Version conflicts | Use dependency:tree to find conflicts, exclude one |
| Hardcoded versions everywhere | Use BOM or <dependencyManagement>
|
Putting credentials in pom.xml
|
Use ~/.m2/settings.xml
|
Not running mvn -U for SNAPSHOTs |
Stale cached versions |
Key Takeaways
-
Scopes control where dependencies are available —
compile,test,provided,runtime. - Maven resolves transitive dependencies automatically — use
dependency:treeto inspect. -
BOM (
<dependencyManagement>) centralizes version management. - Dependencies are cached in
~/.m2/repository. - Use SNAPSHOT for in-progress development, release versions for production.
What’s Next?
Next: Maven Plugins — compiler, surefire, shade, Spring Boot, and how to configure them.