Maven dependency scopes
last modified November 8, 2026
In this article we show how dependency scopes control when a dependency is available in a Maven project. The examples use Maven 3.10 and Java 25.
Maven builds several classpaths for a project: one for compiling the main code, one for compiling and running the tests, and one for running the application. The scope of a dependency decides which of these classpaths it is on, and whether it is passed on to projects that depend on our project.
Overview of dependency scopes
Maven has six dependency scopes.
| Scope | Compile | Test | Runtime | Transitive | Typical use |
|---|---|---|---|---|---|
compile | yes | yes | yes | yes | libraries used by the code; the default |
runtime | no | yes | yes | yes | JDBC drivers, logging backends |
test | no | yes | no | no | testing frameworks |
provided | yes | yes | no | no | APIs supplied by the server, such as the Servlet API |
system | yes | yes | no | no | a JAR from a fixed path; deprecated |
import | — | — | — | — | importing a BOM into dependencyManagement |
The Compile, Test, and Runtime columns tell whether the dependency is on the classpath for compiling the main code, for compiling and running the tests, and for running the application. Tools that package an application with its dependencies, such as the Assembly or Shade plugins, use the runtime classpath.
The example project
Our example project uses one dependency of each common scope.
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>scopes</artifactId>
<version>1.0.0</version>
<properties>
<maven.compiler.release>25</maven.compiler.release>
</properties>
<dependencies>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.15.0</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.20</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.6.6</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
<version>6.1.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>6.1.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.27.7</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
Gson and the SLF4J API have no scope element, so they use the
default compile scope. Logback, the logging backend, is a
runtime dependency. The Servlet API is provided, and JUnit and AssertJ are
test dependencies.
package com.example;
public record User(String name, String occupation) {}
A simple record that we convert to JSON.
package com.example;
import com.google.gson.Gson;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class App {
private static final Logger log = LoggerFactory.getLogger(App.class);
static String toJson(User user) {
return new Gson().toJson(user);
}
public static void main(String[] args) {
log.info("Application started");
var user = new User("John Doe", "gardener");
System.out.println(toJson(user));
log.info("Logging backend: {}", LoggerFactory.getILoggerFactory().getClass().getName());
}
}
The application converts a user to JSON with Gson and logs through the SLF4J API. It also prints the class of the logger factory, which shows which logging backend was found at runtime.
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%-5level %logger{20} - %msg%n</pattern>
</encoder>
</appender>
<root level="info">
<appender-ref ref="STDOUT"/>
</root>
</configuration>
A short Logback configuration that prints the level, the logger name, and the message.
The dependency:tree goal shows all dependencies with their
scopes, including the transitive ones. We come back to the transitive
dependencies at the end of the article.
$ mvn dependency:tree [INFO] com.example:scopes:jar:1.0.0 [INFO] +- com.google.code.gson:gson:jar:2.15.0:compile [INFO] | \- com.google.errorprone:error_prone_annotations:jar:2.50.0:compile [INFO] +- org.slf4j:slf4j-api:jar:2.0.20:compile [INFO] +- ch.qos.logback:logback-classic:jar:1.6.6:runtime [INFO] | \- ch.qos.logback:logback-core:jar:1.6.6:runtime [INFO] +- jakarta.servlet:jakarta.servlet-api:jar:6.1.0:provided [INFO] +- org.junit.jupiter:junit-jupiter:jar:6.1.3:test [INFO] | +- org.junit.jupiter:junit-jupiter-api:jar:6.1.3:test [INFO] | | +- org.opentest4j:opentest4j:jar:1.3.0:test [INFO] | | +- org.junit.platform:junit-platform-commons:jar:6.1.3:test [INFO] | | +- org.apiguardian:apiguardian-api:jar:1.1.2:test [INFO] | | \- org.jspecify:jspecify:jar:1.0.0:test [INFO] | +- org.junit.jupiter:junit-jupiter-params:jar:6.1.3:test [INFO] | \- org.junit.jupiter:junit-jupiter-engine:jar:6.1.3:test [INFO] | \- org.junit.platform:junit-platform-engine:jar:6.1.3:test [INFO] \- org.assertj:assertj-core:jar:3.27.7:test [INFO] \- net.bytebuddy:byte-buddy:jar:1.18.3:test ...
Compile scope
The compile scope is the default. A compile dependency is on all
classpaths, it is packaged with the application, and it is passed on to
projects that depend on ours. Use it for libraries the code calls directly,
such as Gson and the SLF4J API in our example.
We run the application with the Exec plugin, which uses the runtime classpath. The log lines come from Logback, so the runtime dependency was found, although our code never refers to it.
$ mvn -q compile exec:java -Dexec.mainClass=com.example.App
INFO com.example.App - Application started
{"name":"John Doe","occupation":"gardener"}
INFO com.example.App - Logging backend: ch.qos.logback.classic.LoggerContext
Runtime scope
A runtime dependency is needed to run and test the application,
but not to compile it. Our code is written against the SLF4J API, and
Logback is plugged in only at runtime. This keeps the code independent of a
particular logging backend; replacing Logback with another backend means
changing only the POM.
The scope is enforced. In a copy of the project we add a class that uses Logback classes directly.
package com.example;
import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.Logger;
import org.slf4j.LoggerFactory;
public class LogLevels {
public static void quiet() {
var root = (Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME);
root.setLevel(Level.WARN);
}
}
The class casts the SLF4J logger to the Logback logger to change the log
level. Logback is not on the compile classpath, so the compilation fails. If
code really needs the backend classes, the dependency must have the
compile scope.
$ mvn compile [ERROR] /home/user/direct/src/main/java/com/example/LogLevels.java:[3,30] package ch.qos.logback.classic does not exist [ERROR] /home/user/direct/src/main/java/com/example/LogLevels.java:[4,30] package ch.qos.logback.classic does not exist [ERROR] BUILD FAILURE ...
Test scope
A test dependency is available only when compiling and running
the tests. It is not packaged and not passed on to other projects.
package com.example;
import static org.assertj.core.api.Assertions.assertThat;
import org.junit.jupiter.api.Test;
class AppTest {
@Test
void convertsUserToJson() {
var json = App.toJson(new User("Jane Doe", "teacher"));
assertThat(json)
.startsWith("{")
.contains("\"name\":\"Jane Doe\"")
.contains("\"occupation\":\"teacher\"");
}
}
The test uses JUnit and the fluent assertions of AssertJ. Using these
classes in src/main/java would fail to compile.
$ mvn test [INFO] Running com.example.AppTest [INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.198 s -- in com.example.AppTest [INFO] BUILD SUCCESS ...
Provided scope
A provided dependency is needed for compilation, but the runtime
environment supplies it. The typical example is the Servlet API: a servlet
container such as Tomcat or Jetty already contains it, so the web application
must not bring its own copy.
package com.example;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
@WebServlet("/user")
public class HelloServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
resp.setContentType("application/json");
resp.getWriter().println(App.toJson(new User("John Doe", "gardener")));
}
}
The servlet compiles against the Jakarta Servlet API, but the API classes are
not part of the runtime classpath. Note that since Jakarta EE 9 the packages
are jakarta.servlet; the old javax.servlet API is
only for legacy containers.
The includeScope option of dependency:list shows
which dependencies are on a classpath. The compile classpath contains the
compile and provided dependencies; the runtime classpath contains the compile
and runtime dependencies.
$ mvn dependency:list -DincludeScope=compile [INFO] com.google.code.gson:gson:jar:2.15.0:compile -- module com.google.gson [INFO] com.google.errorprone:error_prone_annotations:jar:2.50.0:compile -- module com.google.errorprone.annotations [INFO] org.slf4j:slf4j-api:jar:2.0.20:compile -- module org.slf4j [INFO] jakarta.servlet:jakarta.servlet-api:jar:6.1.0:provided -- module jakarta.servlet ... $ mvn dependency:list -DincludeScope=runtime [INFO] com.google.code.gson:gson:jar:2.15.0:compile -- module com.google.gson [INFO] com.google.errorprone:error_prone_annotations:jar:2.50.0:compile -- module com.google.errorprone.annotations [INFO] org.slf4j:slf4j-api:jar:2.0.20:compile -- module org.slf4j [INFO] ch.qos.logback:logback-classic:jar:1.6.6:runtime -- module ch.qos.logback.classic [INFO] ch.qos.logback:logback-core:jar:1.6.6:runtime -- module ch.qos.logback.core ...
System scope
The system scope works like provided, but the JAR is
taken from a path on the local disk instead of a repository.
<dependencies>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.15.0</version>
<scope>system</scope>
<systemPath>${project.basedir}/lib/gson-2.15.0.jar</systemPath>
</dependency>
</dependencies>
The systemPath element gives the location of the JAR file.
Maven 3.10 still builds such a project, but it warns that the scope is
deprecated, and since 3.10 it rejects system dependencies in POMs that come
from a repository. Instead of the system scope, install the JAR into a
repository, for example with mvn install:install-file.
$ mvn compile
[WARNING] 'dependencies.dependency.scope' for com.google.code.gson:gson:jar declares usage of deprecated 'system' scope @ line 20, column 20
[WARNING] 'dependencies.dependency.systemPath' for com.google.code.gson:gson:jar should not point at files within the project directory, ${project.basedir}/lib/gson-2.15.0.jar will be unresolvable by dependent projects @ line 21, column 25
[INFO] BUILD SUCCESS
...
Import scope
The import scope is used only in the
dependencyManagement section, together with the
pom type. It copies the managed dependencies of another POM,
called a BOM (bill of materials), into our project.
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.junit</groupId>
<artifactId>junit-bom</artifactId>
<version>6.1.3</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-launcher</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
The JUnit BOM manages the versions of all JUnit artifacts, so the two test
dependencies are declared without versions. The
junit-platform-launcher artifact is often added for IDEs that
run tests through the JUnit Platform. Importing a BOM does not add any
dependency by itself; it only supplies versions. See the
Maven BOM article for more.
$ mvn dependency:tree [INFO] com.example:import-scope:jar:1.0.0 [INFO] +- org.junit.jupiter:junit-jupiter:jar:6.1.3:test [INFO] | +- org.junit.jupiter:junit-jupiter-api:jar:6.1.3:test [INFO] | | +- org.opentest4j:opentest4j:jar:1.3.0:test [INFO] | | \- org.junit.platform:junit-platform-commons:jar:6.1.3:test [INFO] | +- org.junit.jupiter:junit-jupiter-params:jar:6.1.3:test [INFO] | \- org.junit.jupiter:junit-jupiter-engine:jar:6.1.3:test [INFO] \- org.junit.platform:junit-platform-launcher:jar:6.1.3:test [INFO] +- org.junit.platform:junit-platform-engine:jar:6.1.3:test [INFO] +- org.apiguardian:apiguardian-api:jar:1.1.2:test [INFO] \- org.jspecify:jspecify:jar:1.0.0:test ...
Scope transitivity
When a dependency has its own dependencies, Maven derives the scope of each transitive dependency from both scopes. The rows of the table show the scope of our direct dependency; the columns show the scope the transitive dependency has in that dependency's POM.
| compile | provided | runtime | test | |
|---|---|---|---|---|
| compile | compile | — | runtime | — |
| provided | provided | — | provided | — |
| runtime | runtime | — | runtime | — |
| test | test | — | test | — |
Provided and test dependencies of a library are never passed on, which is
shown by the dashes. Our dependency tree illustrates the other rules:
logback-core is a compile dependency of Logback, which we
declared with the runtime scope, so it becomes a runtime dependency. The
dependencies of JUnit and AssertJ become test dependencies.
Best practices
- Use the
testscope for all testing libraries. - Use the
runtimescope for implementations that the code reaches only through an API, such as JDBC drivers and logging backends. - Use the
providedscope for APIs that the runtime environment supplies. - Avoid the
systemscope; put the JAR into a repository. - Run
mvn dependency:treeto check the effective scopes of transitive dependencies.
Source
Introduction to the dependency mechanism
In this article we have shown how dependency scopes control the classpaths of a Maven project.
Author
List all Java tutorials.