Dependency Scopes

Understanding when and where dependencies are available

← Back to Index

What are Dependency Scopes?

Think of scopes like access passes at an 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!
}
Why Not compile?

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>
Why Avoid system Scope?
  • 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>
Note

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 vs api
  • implementation: Dependency is internal, not exposed to consumers
  • api: Dependency is part of your public API, exposed to consumers
  • Prefer implementation for 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