Dependency Management
Dependencies are the reason most people pick up Maven in the first place. Instead of hunting down JAR files on the internet, you declare what you need and Maven fetches it — along with everything that library needs in turn. This chapter shows you how to add dependencies, control their scope, and resolve conflicts when multiple versions of the same library show up.
Prerequisites
- A working Maven project with a
pom.xml - Internet connection to reach Maven Central
Adding Your First Dependency
Let us walk through a concrete example. Suppose you want to parse a JSON string and read a field value. You will use Jackson, the most popular JSON library in the Java ecosystem.
Finding the Coordinates
Every dependency is identified by its GAV coordinates. For Jackson, the core databind module is:
| Element | Value |
|---|---|
groupId | com.fasterxml.jackson.core |
artifactId | jackson-databind |
version | 2.17.0 |
You can find these coordinates on Maven Central or by searching for the library name plus "maven dependency."
Declaring the Dependency
Open your pom.xml and add a <dependencies> block inside <project>:
<dependencies>
<!-- Jackson for JSON parsing -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.17.0</version>
</dependency>
</dependencies>Save the file. If you are using IntelliJ IDEA, click the Maven reload icon that appears in the top-right corner of the editor, or press the reload shortcut.
Maven downloads jackson-databind and its transitive dependencies (in this case, jackson-core and jackson-annotations) into ~/.m2/repository.
Writing the Code
Create a new Java class in src/main/java/com/example/JsonDemo.java:
package com.example;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
public class JsonDemo {
public static void main(String[] args) throws Exception {
// Create a reusable JSON parser instance
ObjectMapper mapper = new ObjectMapper();
// A sample JSON string representing a user
String json = "{\"name\":\"Alice\",\"age\":30,\"city\":\"Seattle\"}";
// Parse the string into a tree model
JsonNode root = mapper.readTree(json);
// Extract individual field values
String name = root.get("name").asText();
int age = root.get("age").asInt();
String city = root.get("city").asText();
// Print the extracted values
System.out.println("Name: " + name);
System.out.println("Age: " + age);
System.out.println("City: " + city);
}
}Compile and run:
# Compile the project (Maven downloads dependencies automatically)
mvn compile
# Run the main class
mvn exec:java -Dexec.mainClass="com.example.JsonDemo"You should see:
Name: Alice
Age: 30
City: SeattleTip
Transitive Dependencies at Work
Notice that you only declared jackson-databind, yet the build succeeded. That is because jackson-databind depends on jackson-core and jackson-annotations, and Maven pulled those in automatically. You did not have to hunt them down yourself.
Dependency Scopes
Not every dependency belongs on the classpath at all times. Maven provides six scopes that control when a dependency is available.
The Six Scopes
| Scope | Available During | Typical Use |
|---|---|---|
compile | Compile, test, and runtime | Default. Standard libraries your code needs. |
provided | Compile and test only | Libraries the runtime environment already supplies, such as servlet-api in a Tomcat container. |
runtime | Test and runtime only | Libraries required to run but not to compile, such as JDBC drivers. |
test | Test compilation and execution only | Testing frameworks like JUnit. |
system | Compile and test, like provided | Deprecated. Points to a local JAR file via <systemPath>. |
import | POM inheritance only | Imports a BOM (Bill of Materials) into <dependencyManagement>. |
Example: Mixing Scopes
<dependencies>
<!-- Available everywhere (default scope is compile) -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.17.0</version>
</dependency>
<!-- Only needed for tests -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.10.0</version>
<scope>test</scope>
</dependency>
<!-- Provided by the container at runtime -->
<dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
<version>6.0.0</version>
<scope>provided</scope>
</dependency>
</dependencies>Warning
Do Not Use system Scope
The system scope was a workaround for local JAR files before repositories were common. It breaks portability because the <systemPath> is absolute and machine-specific. Use a repository or install the JAR into your local repository with mvn install:install-file instead.
Dependency Conflicts and Resolution
How Conflicts Arise
Imagine your project directly depends on Library A version 1.0 and Library B version 2.0. Unbeknownst to you, Library B internally depends on Library A version 1.5. Now two versions of Library A are on the classpath. Which one wins?
Maven resolves this automatically using two rules:
- Nearest definition — the version closest to your project in the dependency tree wins.
- First declaration — if two versions are at the same depth, the one declared first in your
pom.xmlwins.
Visualizing the Tree
Run this command to see the full dependency hierarchy:
# Print the dependency tree
mvn dependency:treeFor the Jackson example, the output looks something like this:
com.example:demo:jar:1.0-SNAPSHOT
\- com.fasterxml.jackson.core:jackson-databind:jar:2.17.0:compile
+- com.fasterxml.jackson.core:jackson-annotations:jar:2.17.0:compile
\- com.fasterxml.jackson.core:jackson-core:jar:2.17.0:compileNotice the indentation. Each level shows a transitive dependency. If a version conflict existed, Maven would mark the losing version with (omitted for conflict with X.Y.Z).
Excluding Transitive Dependencies
Sometimes you need to block a transitive dependency entirely. Perhaps it has a known security vulnerability, or you want to substitute a different implementation.
Exclude jackson-annotations from the jackson-databind dependency like this:
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.17.0</version>
<exclusions>
<exclusion>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
</exclusion>
</exclusions>
</dependency>Tip
When to Exclude
Use exclusions sparingly. They are appropriate when a transitive dependency is unsafe, unnecessary, or replaced by a direct dependency you declare yourself. Overusing them makes your build fragile and hard to maintain.
Centralizing Versions with dependencyManagement
In a large project — especially a multi-module one — you do not want version numbers scattered across dozens of pom.xml files. The <dependencyManagement> block lets you declare versions in one place without actually adding the dependency to the classpath.
<dependencyManagement>
<dependencies>
<!-- Define the version here -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.17.0</version>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!-- Use the dependency without a version -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<!-- No version tag needed; inherited from dependencyManagement -->
</dependency>
</dependencies>This pattern is the foundation of BOMs (Bills of Materials). Frameworks like Spring Boot publish a BOM that pins compatible versions for dozens of libraries. You import the BOM once, then add dependencies without worrying about version mismatches.
FAQ
How do I find the latest version of a library?
Search Maven Central or mvnrepository.com. Both show version history, release dates, and usage statistics. Pick a stable release (not a beta or RC) unless you need bleeding-edge features.
Why does Maven download the same dependency every time I build?
It should not. Maven caches artifacts in ~/.m2/repository. The only exceptions are snapshot versions (-SNAPSHOT), which Maven refreshes periodically by design. If you see repeated downloads for release versions, check your network settings or repository configuration.
Can I use a dependency that is not published to Maven Central?
Yes. You can install it manually into your local repository:
# Install a local JAR into the Maven cache
mvn install:install-file \
-Dfile=my-library.jar \
-DgroupId=com.mycompany \
-DartifactId=my-library \
-Dversion=1.0.0 \
-Dpackaging=jarFor team projects, a better solution is a private repository server such as Nexus or Artifactory.
What is a BOM and how do I import one?
A BOM (Bill of Materials) is a special POM that lists dependency versions without declaring the dependencies themselves. Import it like this:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>3.2.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>After importing, any Spring Boot dependency you add can omit the version tag because the BOM provides it.
What happens if two dependencies require incompatible versions?
Maven picks one based on the nearest-definition rule. If the chosen version breaks the other library, you have a few options: upgrade the library that depends on the older version, downgrade the direct dependency, or use an exclusion and declare the working version explicitly. There is no magic — only careful version management.