Declaring dependencies and scopes

Maven · Essentials · 10 min

Scopes decide what ends up on the compile classpath and in your packaged artifact.

Dependencies are listed under <dependencies>, each with coordinates and an optional scope:

  • compile (default): needed to compile, test and run; propagates to consumers of your artifact.
  • provided: needed to compile but supplied at run time by the container or platform (servlet API, Lombok, annotation processors). Not packaged, not transitive.
  • runtime: not needed to compile, needed to run and test (JDBC drivers, logging implementations).
  • test: only for compiling and running tests (JUnit, Mockito, AssertJ).
  • import: only in dependencyManagement, to import a BOM (next lessons).
  • system is deprecated; avoid it.

Maven downloads artifacts from Maven Central by default (plus any <repositories> you add) into the local repository ~/.m2/repository, which is shared by all your projects. mvn dependency:tree prints the resolved graph, mvn dependency:analyze flags used-but-undeclared and declared-but-unused dependencies, and mvn dependency:resolve -U forces a re-check of snapshots.

Transitive dependencies are included automatically with scopes adjusted: a compile dependency's runtime dependencies become runtime for you, and test/provided ones are not inherited at all.

Prefer fixed versions. Version ranges ([1.2,2.0)) make builds non-reproducible. Keep versions in <properties> (<junit.version>5.11.4</junit.version>) or in dependencyManagement so they're declared once.

Example

<properties>
  <junit.version>5.11.4</junit.version>
</properties>
<dependencies>
  <dependency>                               <!-- compile scope (default) -->
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.18.3</version>
  </dependency>
  <dependency>                               <!-- only at run time -->
    <groupId>org.postgresql</groupId>
    <artifactId>postgresql</artifactId>
    <version>42.7.5</version>
    <scope>runtime</scope>
  </dependency>
  <dependency>                               <!-- compile-time only, not packaged -->
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.36</version>
    <scope>provided</scope>
  </dependency>
  <dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter</artifactId>
    <version>${junit.version}</version>
    <scope>test</scope>
  </dependency>
</dependencies>
<!-- mvn dependency:tree      mvn dependency:analyze -->

Remember

  • compile is the default and propagates to consumers
  • provided and runtime split compile-time from run-time needs
  • test scope keeps test libraries out of the artifact
  • dependency:tree and dependency:analyze inspect the graph

Watch out for

  • Declaring a JDBC driver with compile scope and accidentally coding against driver-specific classes.

Check yourself

  1. Which scope should the JUnit library have?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: maven.apache.org/guides/introduction/introduction-to-dependency-mechanism.html