Chapter 06Lesson 01~120 minutes

Maven Dependencies, Scopes, Transitive Resolution, Exclusions, Optional Dependencies, and Classpaths: Concepts, Architecture, and Mental Model

Build a precise mental model of Maven dependency scopes, transitive resolution, exclusions, optional edges, mediation, and the compile/test/runtime classpaths they create.

Maven DependenciesScopesTransitivityClasspathsMediation

Learning objectives

  • Explain how Maven turns dependency declarations into resolved compile, test, and runtime classpaths.
  • Distinguish compile, provided, runtime, test, system, and import without treating every scope as a simple visibility flag.
  • Explain direct versus transitive edges, dependency mediation, path-scoped exclusions, and optional dependencies.
  • Identify which state lives in pom.xml, remote repository metadata/artifacts, the local repository, and generated classpath evidence.
  • Use dependency inspection before changing scopes or deleting caches.
Current baseline — verified 2026-08-23. The production teaching path uses Maven 3.9.16, Maven Wrapper 3.3.4, Maven Dependency Plugin 3.11.0, JDK 21, and Java 17 bytecode/API targeting. All labs use a disposable project directory plus -Dmaven.repo.local=<lab>/.lab-m2/repository; normal ~/.m2 state is never deleted. Network access is used only for immutable public dependencies. A warm-cache replay is optional; the conceptual path remains understandable from the supplied expected evidence.

1. The practical problem — “it is in the POM” does not mean “it is on every classpath”

Chapter 05 traced Maven from the effective POM into lifecycle execution. Dependency declarations add another graph between the model and the compiler/test/runtime processes. A dependency can be visible while compiling but absent when the application runs; visible only to tests; present only because another dependency requested it; removed along one path by an exclusion; or intentionally withheld from downstream consumers through optional=true.

The operational question is therefore not merely “which dependencies are declared?” It is: which artifact version was resolved, through which path, under which scope, and on which classpath? CI and production failures often live in the difference between those answers.

2. Declared dependency → resolved graph → classpath

Dependency resolution and classpath projection
flowchart TD
  P[pom.xml direct dependencies] --> G[Dependency graph collection]
  M[Dependency metadata/POMs] --> G
  R[Remote repositories] --> M
  G --> D[Conflict mediation + exclusions + optional rules]
  D --> C[Compile classpath]
  D --> T[Test classpath]
  D --> U[Runtime classpath]
  R --> L[Local repository cache]
  L --> G
  C --> JC[javac main]
  T --> JT[javac/tests]
  U --> JVM[application JVM]

The arrows separate concerns. Repository metadata describes edges; artifact bytes satisfy selected nodes. Maven collects and mediates a dependency graph, applies exclusion/optional/scope rules, and then projects appropriate portions into classpaths. The same resolved graph can therefore produce different compile, test, and runtime views.

3. Scope is a classpath and transitivity contract

Scope Main compile Test compile/run Application runtime Transitive to consumer? Meaning
compile (default) Yes Yes Yes Yes Normal production dependency. If no scope is written, Maven uses compile.
provided Yes Yes No No Needed to compile/test, but the JDK/container/runtime environment is expected to supply it.
runtime No Yes Yes Yes under Maven scope propagation rules Implementation needed at execution but not to compile main sources.
test No Yes No No Test-only code and test execution.
system Yes Yes No in normal dependency resolution Like provided, but points at an explicit local file with systemPath; repository resolution is bypassed.
import Not a classpath scope Not a classpath scope Not a classpath scope N/A Only valid for a pom-typed dependency inside dependencyManagement; imports managed dependency declarations.

Two nuances matter. First, the Dependency Plugin’s includeScope option is a classpath threshold, not an exact textual filter: for example runtime includes compile + runtime artifacts. Second, provided is an external-runtime contract, not “compileOnly but magically available in production.” Operations must prove the actual deployment environment supplies it.

4. Direct and transitive dependencies form paths

If project A directly declares B and B declares C, Maven may place C on A’s classpath even though A never named C. That is transitive resolution. It saves repeated declarations but creates a coupling: an update to B’s metadata can change which C version/path is present.

scope-app
+- org.apache.commons:commons-text:1.10.0          (direct compile)
|  \- org.apache.commons:commons-lang3:3.12.0     (transitive request)
\- org.apache.commons:commons-lang3:3.17.0        (direct compile)

Resolved classpath: one commons-lang3 version, selected by Maven mediation.

Maven’s dependency mediation selects one version when multiple paths request different versions. The classic rule is “nearest definition”; if two candidate versions are at the same depth, declaration order can break the tie. A direct declaration is depth one, so it normally wins over a conflicting transitive request deeper in the graph. Chapter 11 later centralizes version governance with dependencyManagement and BOMs; here the goal is to see the graph before managing it.

5. Exclusions remove one graph path, not a coordinate globally

An exclusion is attached to the dependency edge that brings an unwanted transitive dependency into the graph. It says, “when traversing through this direct dependency, do not continue to that group/artifact.” Maven deliberately does not model exclusion as a global ban because another path may legitimately need the same dependency.

<dependency>
  <groupId>org.apache.commons</groupId>
  <artifactId>commons-text</artifactId>
  <version>1.10.0</version>
  <exclusions>
    <exclusion>
      <groupId>org.apache.commons</groupId>
      <artifactId>commons-lang3</artifactId>
    </exclusion>
  </exclusions>
</dependency>

<!-- If this application uses lang3 directly, state that contract explicitly. -->
<dependency>
  <groupId>org.apache.commons</groupId>
  <artifactId>commons-lang3</artifactId>
  <version>3.17.0</version>
</dependency>

That pattern is stronger than “exclude until the build becomes green”: the exclusion removes a path, while the direct dependency records the application’s own requirement and selected version.

6. Optional changes downstream propagation, not the producer’s own build

When library A marks dependency B optional, B remains a normal dependency while building/running A’s optional feature. The difference appears when consumer X depends on A: Maven does not bring B into X automatically. If X uses the feature that needs B, X must declare B itself.

Optional edge at the consumer boundary
flowchart LR
  B[commons-codec] -. optional .-> A[optional-feature-lib]
  A --> X[scope-app consumer]
  B -. not propagated .-> X
  X -->|declare directly if feature used| B

Official Maven guidance describes optional dependencies as a stop-gap when a feature cannot be split into a separate module. A separate feature module is often clearer because consumers opt into a coordinate rather than learning that one class inside a broad library activates a hidden optional runtime requirement.

7. Know which state store can explain a surprising classpath

State store What it tells you What it does not prove
Project pom.xml Direct scope, exclusions, optional flags on this project, dependencyManagement declarations. The final selected graph after all transitive metadata and mediation.
Effective POM Inherited/active dependency declarations and management. Artifact integrity or runtime environment contents.
Remote dependency POM/metadata Transitive edges, optional flags, versions/metadata published by producers. That bytes are trustworthy or vulnerability-free.
Local repository Cached POM/JAR bytes and local installs used for resolution. That the source declaration still expects those cached files.
dependency:tree Resolved dependency hierarchy used by Maven. The OS/container will supply a provided dependency.
dependency:build-classpath Concrete artifact paths for a chosen classpath threshold. That application startup will exercise every artifact successfully.

8. Read-only inspection before changing a dependency

Use fully versioned plugin coordinates in reproducibility-sensitive evidence so the diagnostic tool itself does not drift.

REPO="$PWD/.lab-m2/repository"
./mvnw -Dmaven.repo.local="$REPO" --version

./mvnw -Dmaven.repo.local="$REPO"   org.apache.maven.plugins:maven-help-plugin:3.5.2:effective-pom   -Doutput=target/effective-pom.xml

./mvnw -Dmaven.repo.local="$REPO"   org.apache.maven.plugins:maven-dependency-plugin:3.11.0:tree   -DoutputFile=target/dependency-tree.txt

./mvnw -Dmaven.repo.local="$REPO"   org.apache.maven.plugins:maven-dependency-plugin:3.11.0:build-classpath   -DincludeScope=runtime -Dmdep.outputFile=target/runtime.cp

None of these commands should be followed by “delete ~/.m2 and try again.” Preserve the model/tree/classpath first. If cache state is suspected later, compare against a second isolated local repository.

9. Why this matters in DevOps

Dependency scope is part of the deployment contract. A CI compile can be green while a production JVM fails because a provided or optional dependency is absent. An over-broad compile dependency can inflate containers and increase vulnerability surface. A test-only library can make local tests pass while production code relies on behavior that never exists in the runtime image. Production evidence should therefore include the resolved graph and—when runtime incidents matter—the effective runtime classpath or packaged dependency inventory.

Knowledge check

A dependency is omitted from <scope>. Which scope does Maven use?

A Servlet API dependency is provided. Maven compiles successfully. Does that prove production can start?

Library A marks B optional. Consumer X depends on A. Why is B absent from X’s dependency tree?

Why should an exclusion normally be attached to a specific dependency edge rather than treated as a global ban?

What is the first artifact-level inspection when a runtime class is missing?

Summary

Maven dependencies are graph edges with scope and propagation semantics, not a flat list of JARs. Compile, provided, runtime, test, system, and import have different meanings. Transitivity expands the graph; mediation selects versions; exclusions remove a path; optional dependencies stop propagation to consumers. The resolved graph is then projected into compile/test/runtime classpaths. Reliable build engineering records and inspects those projections rather than assuming a successful compile proves a correct runtime.

Next lesson

Turn scope theory into classpath evidence

Lesson 2 builds two disposable Maven projects, installs an optional-feature library into an isolated local repository, and proves the resolved graph and three classpath views.

Official references and version notes

Version-sensitive statements in this lesson were checked against Apache Maven primary documentation on 2026-08-23. Required labs use Maven 3.9.16 through Maven Wrapper 3.3.4, Maven Dependency Plugin 3.11.0 for graph/classpath inspection, JDK 21 to run Maven, and Java 17 as the application release target. Maven 4.0.0-rc-6 remains preview-stage and is not required here.

Keep the academy open

Support free, practical DevOps education.

Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.

Ethereum / ERC-20
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0 Send only Ethereum/ERC-20 compatible assets to this address.