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.
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.
-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.
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?
Verify whether the real deployment environment is actually responsible for supplying that dependency. If there is no such provider, the model is wrong; if there is, reproduce in that target environment rather than a plain JVM.
An optional dependency is missing from a consumer runtime. Should you delete the local repository?
No. Optional is a propagation rule. Inspect producer metadata and the consumer tree; add the dependency directly if the consumer uses the optional feature.
A test passes because a test-scoped implementation is present, but production startup fails. What evidence compares the environments?
Compare generated test and runtime classpaths, then execute a production-style startup with only runtime dependencies.
Why can removing a vulnerable transitive dependency with an exclusion create a new incident?
If the parent library still executes code requiring that transitive library, the exclusion creates a classpath hole. Upgrade/replace/manage deliberately instead of assuming exclusion is remediation.
How do you investigate a suspected corrupt Maven cache without destroying evidence?
Rebuild against a separate empty isolated local repository and compare resolution/tree/checksum evidence with the original isolated repository.
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.
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.
- Maven — Introduction to the Dependency Mechanism
- Maven — Optional Dependencies and Dependency Exclusions
- Maven — Dependency and Repository Model
- Maven — POM Reference
- Maven Dependency Plugin 3.11.0 — Usage
- Maven Dependency Plugin 3.11.0 — Plugin Details
- Maven Dependency Plugin — dependency:tree
- Maven Dependency Plugin — dependency:build-classpath
- Maven Releases History
- Apache Maven Wrapper
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.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0
Send only Ethereum/ERC-20 compatible assets to this
address.