Chapter 06Lesson 04~135 minutes

Maven Dependencies, Scopes, Transitive Resolution, Exclusions, Optional Dependencies, and Classpaths: Diagnostics, Failure Modes, Security, and Performance

Diagnose classpath and transitivity failures systematically, including missing runtime classes, test-only assumptions, over-broad exclusions, system-scope machine coupling, and optional dependencies that consumers incorrectly assume will transit.

DiagnosticsNoClassDefFoundErrorSystem ScopeOptional EdgeCache Safety

Learning objectives

  • Apply a repeatable diagnostic sequence to Maven classpath failures instead of changing scopes by trial and error.
  • Diagnose compile-success/runtime-failure cases caused by provided or optional dependencies.
  • Recognize test-only assumptions, over-broad exclusions, and system-scope machine coupling.
  • Use an isolated fresh Maven repository to distinguish cache effects from model errors without deleting normal user state.
  • Repair the least amount of dependency model and independently verify the runtime/classpath result.
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 dependency diagnostic sequence

Dependency incidents are easiest to solve when evidence is collected before the graph is mutated. Use the same sequence in a developer shell and CI job:

Step Question Evidence
1. Preserve concise failure What class/artifact/path actually failed? Exception + first relevant Caused by; failed Maven goal; test report.
2. Confirm identity Which wrapper/Maven/JDK executed? ./mvnw --version, wrapper properties/checksum.
3. Inspect declared/effective model What scopes/exclusions/optional flags are active? pom.xml, help:effective-pom.
4. Inspect resolved graph Which path/version survived mediation? Version-pinned dependency:tree.
5. Inspect classpath projection Is the provider JAR actually on the failing runtime/test/compile classpath? Version-pinned dependency:build-classpath.
6. Inspect repository/cache state Are metadata/JAR bytes available from the expected origin? Isolated local-repository path and file checksums where needed.
7. Correct minimally Which scope/path/direct declaration is wrong? Small POM diff.
8. Verify controlled rebuild Did graph + classpath + runtime behavior all change as predicted? Fresh/repeat build + runtime/test evidence.

2. Failure mode — compile succeeds, runtime cannot load a provided class

The Chapter 02/06 ContainerOnlyEndpoint compiles because jakarta.servlet-api is provided. A plain java process does not represent the servlet container, so the runtime classpath intentionally lacks that JAR.

REPO="$PWD/.lab-m2/repository"
DEP=org.apache.maven.plugins:maven-dependency-plugin:3.11.0

./mvnw -Dmaven.repo.local="$REPO" -f scope-app/pom.xml "$DEP":tree   -Dincludes=jakarta.servlet:jakarta.servlet-api

./mvnw -Dmaven.repo.local="$REPO" -f scope-app/pom.xml "$DEP":build-classpath   -DincludeScope=runtime -Dmdep.outputFile=target/runtime.cp

grep -q 'jakarta.servlet-api' scope-app/target/runtime.cp   && echo 'unexpected: servlet API present'   || echo 'expected: servlet API absent from Maven runtime classpath' 

If a real deployment uses a servlet container, this absence is correct. If the application is actually a standalone Java process, the model is wrong: the dependency should normally be part of the application runtime rather than provided. Do not “fix” the standalone command by copying a random JAR onto the machine.

3. Intentionally broken example — optional feature assumed to transit

The consumer calls DigestFeature.sha256() from optional-feature-lib. The producer’s implementation uses Commons Codec, but Codec is optional in producer metadata. Consumer compilation can still succeed because Codec types do not appear in the public method signature. Runtime loads the implementation and then fails when DigestUtils is needed.

CP="scope-app/target/classes:$(cat scope-app/target/runtime.cp)"
set +e
java -cp "$CP" dev.academy.OptionalDemo   > scope-app/target/optional-demo.out 2> scope-app/target/optional-demo.err
status=$?
set -e
printf 'exit=%s
' "$status"
sed -n '1,16p' scope-app/target/optional-demo.err

./mvnw -Dmaven.repo.local="$REPO" -f scope-app/pom.xml "$DEP":tree   -Dincludes=commons-codec:commons-codec

Interpretation: the empty tree filter and missing class are consistent with the producer’s optional edge. This is not evidence of a corrupt cache. If the consumer needs the digest feature, add Codec directly. If the feature should always work for every consumer, the producer’s optional design is probably wrong.

4. Failure mode — test-only dependency becomes an undocumented production assumption

Test scope is intentionally invisible to main compilation and application runtime. A common leak is subtler than “main source imports JUnit”: tests bootstrap an implementation, filesystem fixture, logging binding, or helper library that production never supplies, so behavior observed under mvn test is mistaken for production behavior.

Diagnose by comparing test and runtime classpaths and by executing a production-style startup check with only the runtime classpath. Do not move every test dependency to compile to make the difference disappear.

./mvnw -Dmaven.repo.local="$REPO" -f scope-app/pom.xml "$DEP":build-classpath   -DincludeScope=test -Dmdep.outputFile=target/test.cp
./mvnw -Dmaven.repo.local="$REPO" -f scope-app/pom.xml "$DEP":build-classpath   -DincludeScope=runtime -Dmdep.outputFile=target/runtime.cp

# Inspect which test-only artifacts disappear from runtime.
tr ':' '
' < scope-app/target/test.cp | sort > scope-app/target/test.paths
tr ':' '
' < scope-app/target/runtime.cp | sort > scope-app/target/runtime.paths
comm -23 scope-app/target/test.paths scope-app/target/runtime.paths

5. Failure mode — an exclusion removes something the selected feature really needs

Exclusions act below the dependency edge where they are declared. If you exclude a transitive artifact simply because a scanner highlighted it, code inside the parent dependency can later fail when it reaches the feature that needed the excluded library.

1. Which dependency path originally brought the missing coordinate?
2. Is the missing library actually unused by the feature we execute?
3. Did we exclude the coordinate because of a version conflict, a policy ban, or genuine non-use?
4. Should the repair be:
   - remove the exclusion,
   - add an intentional direct dependency,
   - manage the version,
   - upgrade/replace the parent dependency,
   - or disable/remove the feature?

Security remediation should normally upgrade or replace vulnerable code, not create a classpath hole. An exclusion is justified only when the remaining code path does not require the excluded dependency or when an explicit replacement supplies the needed API/behavior.

6. Failure mode — system scope works on one machine

With system scope, Maven checks an explicit filesystem path instead of resolving from repositories. A developer machine may have the JAR while an ephemeral Linux CI agent does not. The failure is deterministic once the path difference is recognized; cache deletion is irrelevant.

./mvnw --version
# Inspect the effective POM for system-scoped dependencies:
./mvnw org.apache.maven.plugins:maven-help-plugin:3.5.2:effective-pom   -Doutput=target/effective-pom.xml

grep -n -A5 -B3 '<scope>system</scope>' target/effective-pom.xml || true
# Then record whether the referenced systemPath exists on this agent.

The durable repair is to make the artifact available through normal repository coordinates under an authorized repository policy, or replace the dependency—not to copy a binary into a magic absolute path on every agent.

7. Cache suspicion — isolate first, delete later only if evidence demands it

A local repository can mask upstream removal or contain a bad partial/corrupt file. But the safest experiment is a second isolated repository because it preserves the original state for comparison.

REPO_A="$PWD/.lab-m2/repository"
REPO_B="$PWD/.lab-m2-diagnostic/repository"

./mvnw -Dmaven.repo.local="$REPO_A" -f scope-app/pom.xml test   | tee scope-app/target/repo-a.log
./mvnw -Dmaven.repo.local="$REPO_B" -f scope-app/pom.xml test   | tee scope-app/target/repo-b.log

# Compare dependency trees after each run. Do not recursively delete the normal ~/.m2/repository cache.

If A succeeds and B fails to resolve an immutable coordinate, investigate repository origin/availability and the cached artifact’s provenance. If both resolve the same graph but runtime still fails, the problem is likely model/classpath/application behavior rather than repository state.

8. Performance — separate resolution time from build/runtime correctness

A warm local repository reduces metadata/artifact download work. It does not change the intended scope semantics for immutable inputs. Measure cold/warm dependency resolution separately from model building, compilation, tests, packaging, and application startup. Do not broaden scopes to “speed up” builds, and do not cache generated target/ output as if it were the same thing as the dependency repository.

9. Security boundaries

Dependencies and plugins are executable supply-chain inputs. A smaller runtime classpath can reduce exposure, but scope selection is not a security scanner and a checksum is not a vulnerability assessment. Repository origin, artifact identity, signatures/checksums where supported, SBOM/scanner evidence, and policy are complementary controls. Never bypass verification or add an untrusted repository simply because a dependency stopped resolving.

Credential rule: none of these labs need repository credentials. If an organization later uses a private repository, keep credentials in Maven settings/CI secret storage and never in pom.xml, command history, or diagnostic output.

Knowledge check

A standalone application compiles against a provided dependency and then fails at startup. What should you verify before changing the scope?

An optional dependency is missing from a consumer runtime. Should you delete the local repository?

A test passes because a test-scoped implementation is present, but production startup fails. What evidence compares the environments?

Why can removing a vulnerable transitive dependency with an exclusion create a new incident?

How do you investigate a suspected corrupt Maven cache without destroying evidence?

Summary

Classpath diagnostics are graph diagnostics. Preserve the original exception, confirm wrapper/Maven/JDK identity, inspect effective declarations, inspect the resolved tree, project the failing classpath, and only then change the model. Provided and optional dependencies can intentionally create compile-success/runtime-absence. Test scope can hide assumptions. Exclusions can create holes. System scope creates machine coupling. Cache suspicion should be tested with isolation, not blind deletion.

Next lesson

Prove the whole dependency contract

Lesson 5 combines multiple scopes, an exclusion, an optional edge, classpath predictions, a controlled runtime failure, a minimal repair, and a cleanup/evidence checklist.

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.