Maven Scopes Explained: Compile, Test, Runtime, Provided, and More
In a Maven project, dependency scopes define where and when a library is available: during compilation, testing, runtime execution, packaging, or only in special cases. A clear understanding of scopes helps teams avoid oversized artifacts, classpath conflicts, and deployment surprises.
TLDR: Maven scopes control a dependency’s visibility across the build lifecycle. For example, a web application may use javax.servlet-api with provided because the application server already supplies it. In a 20-module enterprise project, using correct scopes can reduce packaged artifact size by 15–40% and prevent duplicate libraries from appearing in production. Most projects rely mainly on compile, test, runtime, and provided.
Why Maven Scopes Matter
Maven does more than download JAR files. It builds a dependency graph and decides which libraries belong on each classpath. A dependency needed to compile source code may not be needed in production. A testing framework should not be shipped with the final application. A database driver may not be needed at compile time but must exist when the application starts.
Scopes make these distinctions explicit inside the pom.xml. Without them, projects often become harder to maintain, slower to build, and more vulnerable to version conflicts.
The Default Scope: Compile
The compile scope is Maven’s default. If no scope is declared, Maven assumes compile.
A dependency with compile scope is available:
- During compilation
- During testing
- At runtime
- To dependent projects transitively
This scope is suitable for libraries that application code directly imports and needs everywhere. Common examples include utility libraries, logging APIs, JSON libraries, and domain-specific SDKs.
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.17.0</version>
</dependency>
Because compile dependencies are transitive, they should be used carefully. If a library is only needed for tests or deployment support, a narrower scope is usually better.
Test Scope
The test scope is used for libraries required only to compile and run tests. These dependencies are not included in the main artifact and are not available to production code.
Typical examples include:
JUnitMockitoAssertJ- Embedded test servers
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.10.2</version>
<scope>test</scope>
</dependency>
The test scope keeps production artifacts clean. A team building a payment service, for instance, may have hundreds of test classes and several mocking libraries, but none of those tools should be deployed with the service.
Runtime Scope
The runtime scope means the dependency is not required to compile application code, but it is required when the application runs.
A common example is a database driver. Application code normally depends on JDBC interfaces, while the actual driver implementation is loaded at runtime.
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.7.3</version>
<scope>runtime</scope>
</dependency>
This scope is useful when code compiles against an abstraction but needs a concrete implementation during execution. It helps communicate that the dependency is operational rather than part of the main source API.
Provided Scope
The provided scope is used when a dependency is needed to compile and test the project, but the runtime environment supplies it. Maven does not package provided dependencies into the final artifact.
This is common in Java web applications deployed to servlet containers or application servers. For example, Tomcat provides the Servlet API, so the application should not bundle its own copy.
<dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
<version>6.0.0</version>
<scope>provided</scope>
</dependency>
If provided is used incorrectly, the application may fail outside the expected container. Therefore, it works best when the deployment environment is well known and controlled.
System Scope
The system scope is rarely recommended. It points to a dependency located on the local file system rather than in a Maven repository.
<dependency>
<groupId>com.vendor</groupId>
<artifactId>legacy-sdk</artifactId>
<version>1.0</version>
<scope>system</scope>
<systemPath>${project.basedir}/lib/legacy-sdk.jar</systemPath>
</dependency>
This approach reduces build portability. A project may work on one developer’s machine and fail on another. Modern Maven practice favors installing the JAR into a private repository instead.
Import Scope
The import scope is special because it applies only to dependencies of type pom inside dependencyManagement. It is commonly used to import a Bill of Materials, also called a BOM.
A BOM centralizes versions for a group of related dependencies. Spring Boot, Jakarta EE, and cloud SDKs often provide BOMs so projects can avoid declaring every version manually.
<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>
The import scope does not place a library on the classpath. Instead, it imports dependency version rules.
How Scopes Affect Transitive Dependencies
Transitive dependencies are dependencies brought in by other dependencies. Maven scopes influence whether those transitive libraries appear in downstream projects.
For instance, compile dependencies are generally exposed transitively, while test dependencies are not. This prevents a library’s test framework from leaking into every project that consumes it.
Scope handling becomes especially important in large builds. If ten modules all inherit unnecessary runtime libraries, the final deployment package may include dozens of unused JARs.
Best Practices for Choosing Scopes
- Use
compileonly when code directly needs the library in normal operation. - Use
testfor all testing frameworks and test-only utilities. - Use
runtimefor drivers and implementations loaded during execution. - Use
providedwhen the target server or platform supplies the dependency. - Avoid
systemunless no repository-based alternative exists. - Use BOMs with
importto standardize versions across related libraries.
Common Mistakes
One common mistake is leaving test libraries in the default compile scope. This may cause production artifacts to include unnecessary testing tools. Another mistake is marking a dependency as provided when the runtime environment does not actually include it. In that case, the code compiles but fails at startup with a ClassNotFoundException.
A third issue is using runtime for libraries that source code imports directly. If the compiler needs the classes, the dependency usually belongs in compile.
FAQ
What is the default Maven scope?
The default scope is compile. If a dependency does not declare a scope, Maven treats it as available for compilation, testing, and runtime.
Should JUnit use compile or test scope?
JUnit should normally use test scope because it is required only for compiling and running tests, not for production execution.
What is the difference between runtime and provided?
runtime means the application needs the dependency when it runs. provided means the runtime environment is expected to supply the dependency, so Maven should not package it.
When should import scope be used?
import scope should be used in dependencyManagement when importing a BOM. It helps manage versions consistently across a project.
Is system scope recommended?
No. system scope is generally discouraged because it relies on local file paths and makes builds less portable. A repository-based dependency is usually a better choice.
