What are Dependency Scopes?
Think of scopes like access passes at an event:
- VIP Pass (compile): Access everywhere - backstage, main event, after party
- Staff Pass (provided): Access during work, but you don't take it home
- Event Pass (runtime): Access to the main event only
- Rehearsal Pass (test): Access only during practice, not the real event
Scope determines:
- When the dependency is available (compile, test, runtime)
- Whether it's included in the final package (JAR/WAR)
- How it affects transitive dependencies
Scope Overview
| Scope | Compile | Test | Runtime | Packaged | Transitive |
|---|---|---|---|---|---|
| compile | Yes | Yes | Yes | Yes | Yes |
| provided | Yes | Yes | No | No | No |
| runtime | No | Yes | Yes | Yes | Yes |
| test | No | Yes | No | No | No |
| system | Yes | Yes | No | No | No |
| import | Special: Only for BOM imports in dependencyManagement | ||||
compile (Default)
The default scope. Available everywhere: compile, test, runtime, and packaged in final artifact.
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>6.1.0</version>
<!-- scope defaults to compile if not specified -->
</dependency>
<!-- Same as: -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>6.1.0</version>
<scope>compile</scope>
</dependency>
Use for: Most dependencies - libraries your code directly uses and needs at runtime.
Examples: Spring Framework, Jackson, Apache Commons, Guava
// Your code uses it directly at compile time
import org.springframework.stereotype.Component;
@Component // Needs spring-core at compile AND runtime
public class MyService {
// ...
}
provided
Available at compile time, but NOT packaged. The runtime environment provides it.
<dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
<version>6.0.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
<scope>provided</scope>
</dependency>
Use for: APIs that the server/container provides, or compile-time-only tools.
Examples:
- Servlet API - Tomcat/Jetty provides this
- Lombok - Only needed at compile time for code generation
- Jakarta EE APIs - Application server provides implementations
// Servlet API - you code against it, but Tomcat provides the implementation
import jakarta.servlet.http.HttpServlet;
public class MyServlet extends HttpServlet {
// Code compiles fine
// At runtime, Tomcat provides the actual servlet-api JAR
// If you packaged it, you'd have conflicts!
}
If you include servlet-api in your WAR, it might conflict with Tomcat's version. provided prevents this by not packaging the dependency.
runtime
NOT available at compile time, but available at runtime and packaged.
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.7.0</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.4.14</version>
<scope>runtime</scope>
</dependency>
Use for: Implementation libraries loaded dynamically at runtime.
Examples:
- JDBC Drivers - PostgreSQL, MySQL, Oracle drivers
- SLF4J Implementations - Logback, Log4j2
- JPA Implementations - Hibernate (when coding to JPA API)
// You code against JDBC (standard API)
import java.sql.Connection;
import java.sql.DriverManager;
Connection conn = DriverManager.getConnection(
"jdbc:postgresql://localhost/mydb", "user", "pass"
);
// At runtime, PostgreSQL driver is loaded via ServiceLoader
// You never import org.postgresql.* directly in your code
Why Use runtime Instead of compile?
// BAD: Direct dependency on implementation
import org.postgresql.Driver; // Tight coupling!
// GOOD: Code to interface, runtime provides implementation
import java.sql.Driver; // Standard API
// PostgreSQL driver discovered at runtime
test
Available ONLY during test compilation and execution. Not packaged.
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.10.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>5.8.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>test</scope> <!-- In-memory DB for testing -->
</dependency>
Use for: Testing frameworks, mocking libraries, test utilities.
Examples: JUnit, Mockito, AssertJ, Hamcrest, H2 (test DB), Testcontainers
// Only available in src/test/java
import org.junit.jupiter.api.Test;
import static org.mockito.Mockito.*;
class MyServiceTest {
@Test
void testSomething() {
// Test code here
}
}
// If you tried to use JUnit in src/main/java, it won't compile!
system (Avoid!)
Like provided, but you specify the JAR path manually. Avoid using this.
<dependency>
<groupId>com.example</groupId>
<artifactId>proprietary-lib</artifactId>
<version>1.0</version>
<scope>system</scope>
<systemPath>${project.basedir}/lib/proprietary-lib.jar</systemPath>
</dependency>
- Breaks build portability (absolute paths)
- JAR not in repository, hard to share
- Not packaged in final artifact
- Better alternatives exist
Better Alternatives
# Option 1: Install to local repo
mvn install:install-file \
-Dfile=lib/proprietary-lib.jar \
-DgroupId=com.example \
-DartifactId=proprietary-lib \
-Dversion=1.0 \
-Dpackaging=jar
# Option 2: Use a private Maven repository (Nexus, Artifactory)
# Option 3: Use a file-based repository in your project
import
Special scope for importing BOMs (Bill of Materials) in dependencyManagement.
<dependencyManagement>
<dependencies>
<!-- Import Spring Boot BOM -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>3.2.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- Import JUnit BOM -->
<dependency>
<groupId>org.junit</groupId>
<artifactId>junit-bom</artifactId>
<version>5.10.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<!-- Now use dependencies without specifying versions -->
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
import scope ONLY works in dependencyManagement and ONLY with type=pom.
Transitive Scope Resolution
When your dependency has its own dependencies, scopes interact:
| Transitive Dependency Scope | |||||
|---|---|---|---|---|---|
| Direct Scope | compile | provided | runtime | test | |
| compile | compile | - | runtime | - | |
| provided | provided | - | provided | - | |
| runtime | runtime | - | runtime | - | |
| test | test | - | test | - | |
<!-- Example: spring-boot-starter-web (compile scope) -->
<!-- Has transitive dependency: spring-core (compile scope) -->
<!-- Result: spring-core is compile scope in your project -->
<!-- Example: spring-boot-starter-test (test scope) -->
<!-- Has transitive dependency: mockito-core (compile scope) -->
<!-- Result: mockito-core is test scope in your project -->
Gradle Equivalent Scopes
| Maven Scope | Gradle Configuration |
|---|---|
| compile | implementation or api |
| provided | compileOnly |
| runtime | runtimeOnly |
| test | testImplementation |
// build.gradle.kts
dependencies {
// compile scope equivalent
implementation("org.springframework:spring-core:6.1.0")
// provided scope equivalent
compileOnly("jakarta.servlet:jakarta.servlet-api:6.0.0")
// runtime scope equivalent
runtimeOnly("org.postgresql:postgresql:42.7.0")
// test scope equivalent
testImplementation("org.junit.jupiter:junit-jupiter:5.10.0")
// Gradle-specific: api exposes to consumers (like compile but transitive)
api("com.google.guava:guava:32.1.3-jre")
}
implementation: Dependency is internal, not exposed to consumersapi: Dependency is part of your public API, exposed to consumers- Prefer
implementationfor better encapsulation and faster builds
Common Patterns
<!-- Typical Spring Boot Web Application -->
<dependencies>
<!-- Main dependencies (compile) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- Compile-time only (provided) -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
<!-- Runtime only -->
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Testing -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
Quick Reference
When to Use Each Scope:
- compile (default): Your code imports and uses it directly
- provided: Container/server provides it (Servlet API, Jakarta EE)
- runtime: Needed at runtime but you code to an interface (JDBC drivers)
- test: Only for testing (JUnit, Mockito, test DBs)
- import: BOM imports in dependencyManagement
- system: Avoid - use local repo install instead
Summary
- compile: Default, everywhere, packaged
- provided: Compile time only, server provides at runtime
- runtime: Runtime only, for dynamic implementations
- test: Test only, not packaged
- import: BOM imports for version management
- Choosing correctly: Reduces artifact size, prevents conflicts