Chapter 06Lesson 03~110 minutes

Maven Dependencies, Scopes, Transitive Resolution, Exclusions, Optional Dependencies, and Classpaths: Configuration, Design Choices, and Tradeoffs

Choose Maven dependency scopes, direct declarations, exclusions, and optional boundaries deliberately by connecting each design choice to classpath visibility, published metadata, reproducibility, and operational risk.

Scope DesignDirect DependenciesOptional FeaturesExclusionsReproducibility

Learning objectives

  • Choose Maven scopes according to the real compile/test/runtime contract rather than according to desired transitivity.
  • Prefer direct declarations when application source imports a dependency API, even if that artifact currently arrives transitively.
  • Distinguish exclusions from version management and explain why an exclusion is a path correction rather than a version policy.
  • Choose between an optional dependency and a separate feature module based on consumer clarity and runtime coupling.
  • Evaluate dependency choices through maintainability, reproducibility, security, CI throughput, and upgrade cost.
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. Design from classpath ownership, not from “what makes Maven green”

A scope is not a knob for manipulating dependency precedence. It states where the dependency is required. An exclusion is not a version pin. Optional is not “maybe downloaded.” When these mechanisms are used to solve the wrong problem, the POM can compile today while becoming fragile for consumers, CI agents, or production runtimes.

A useful design test is: which source set or runtime owns this API, and who is responsible for supplying it? The answer should lead to the scope, direct declaration, or module boundary.

2. Scope selection versus classpath leakage

Requirement Best starting scope Why Common mistake
Main source imports and runtime uses API compile Required during both compilation and execution. Using runtime to reduce transitivity; main compilation then fails or relies on another accidental path.
Main source does not import implementation; application loads it at runtime runtime Implementation is execution-only. Making it compile scope because “all libraries are compile.”
Container/JDK supplies compatible API at deployment provided Compile/test need the API; packaged runtime should not supply it. Assuming Maven verifies the external container version.
Only test source/tests use library test Keeps production model/classpath clean. Writing production code that depends on a test-only helper discovered through IDE state.
Local machine JAR outside repositories Avoid system when possible Absolute machine path undermines portability and repository metadata. Using system scope as a substitute for publishing/installing a proper artifact.

3. If your source imports an API, declare the dependency directly

Suppose scope-app imports org.apache.commons.lang3.StringUtils. It currently receives Commons Lang transitively through Commons Text. That compiles, but it means an implementation choice inside Commons Text silently owns an API your source code uses directly.

scope-app source -> StringUtils API
POM: scope-app -> commons-text -> commons-lang3

The source-level dependency and POM-level dependency do not match.
scope-app -> commons-text
scope-app -> commons-lang3   # direct because source imports it

Now the POM states the same dependency that the source code states.

Direct declarations increase POM lines but reduce hidden coupling. They also make upgrades and security remediation easier because ownership is visible. Chapter 11 later provides centralized version governance so direct dependencies need not mean uncontrolled version repetition.

4. Exclusion and version management solve different problems

Use an exclusion when a specific incoming path should not contribute a dependency. Use version management when the coordinate should remain present but its version must be governed consistently. The Chapter 02/03 mental model matters: graph shape and selected version are separate decisions.

Situation Better control Reason
A transitive library is not needed for the feature you use and causes conflict/risk Exclusion on the responsible incoming edge Changes graph reachability along that path.
Several paths legitimately need the same library but request different versions Version management / BOM (Chapter 11) Keeps the node while controlling the selected version.
Application source imports the transitive library API Direct dependency, optionally with management Records ownership; do not rely only on another library’s metadata.
Coordinate must be forbidden everywhere for policy/security reasons Enforcer/policy plus path-specific remediation A global governance requirement is stronger than a single POM exclusion.

5. Optional dependency versus a feature module

Optional dependencies are useful when one artifact contains code paths that require extra libraries but many consumers never use them. The producer can compile those paths while consumers avoid automatic transitive baggage. The cost is discoverability: a consumer can compile against the producer and then fail only when an optional feature executes.

Design Consumer experience Operational tradeoff
One JAR + optional dependency Consumer adds the optional library only when activating the feature. Fewer modules, but the feature contract is partly implicit and runtime failures can surprise consumers.
Core JAR + feature JAR Consumer opts into feature-codec coordinate; its normal dependencies transit. More modules/releases, but dependency intent and runtime requirements are explicit.

Official Maven guidance calls optional dependencies and exclusions stop-gap mechanisms in some designs. If the feature can cleanly become its own module, that often makes the dependency graph easier to reason about.

6. Provided scope moves responsibility outside Maven

provided is a cross-system contract. Maven makes the API visible to compile/test but deliberately omits it from the runtime dependency classpath. The servlet container, application server, JDK, or deployment platform must supply a compatible implementation/API. Therefore the POM alone cannot prove deployment compatibility.

In CI, pair a provided dependency with environment evidence: target runtime/container version, compatibility test, integration test, or container image inventory. If you control neither the supplied version nor the deployment contract, provided can create a silent environmental dependency.

7. System scope is a portability warning, not a convenience repository

system points directly at a local file via systemPath and bypasses repository lookup. The path must be absolute. That means another developer or ephemeral CI agent must reproduce the same file path outside Maven’s normal coordinate/repository model.

<dependency>
  <groupId>com.example.vendor</groupId>
  <artifactId>legacy-driver</artifactId>
  <version>1.0</version>
  <scope>system</scope>
  <systemPath>${project.basedir}/vendor/legacy-driver.jar</systemPath>
</dependency>

Even a project-relative expression eventually resolves to a file rather than an authoritative repository artifact. Prefer installing/publishing a properly governed coordinate to a repository under your organization’s policy. Do not commit a proprietary/vendor JAR merely to avoid repository management.

8. Worked decision table — a service migrating into CI

Observation Decision Evidence to require
Service imports Commons Lang directly, but it arrives through Commons Text. Declare Lang directly; manage version intentionally. Source import + dependency tree show ownership and selected version.
JDBC driver is only selected via Class.forName. Runtime scope is appropriate if no compile-time driver API is used. Compile succeeds without driver; runtime classpath contains it; startup probe succeeds.
Servlet API comes from the target servlet container. Provided scope, plus deployment compatibility evidence. Compile/test classpath contains API; runtime Maven classpath omits it; container test supplies it.
Digest support used by 5% of consumers. Optional may work; feature module is cleaner if API surface is distinct. Consumer dependency tree and feature activation test.
Security team says a transitive coordinate is banned everywhere. Central policy/enforcer + remediate each path; do not pretend one exclusion is global. Policy failure plus dependency trees showing no remaining paths.

9. Tradeoffs through a DevOps lens

  • Maintainability: direct declarations mirror source ownership; hidden transitives make upgrades surprising.
  • Reproducibility: fixed coordinates and explicit graph controls are easier to compare across CI agents.
  • Security: smaller, intentional runtime classpaths reduce attack/vulnerability surface, but scope alone is not vulnerability analysis.
  • Developer experience: optional edges reduce automatic downloads but can move failures from build time to feature runtime.
  • CI throughput: fewer unnecessary dependencies reduce resolution and scanning work; do not trade correctness for a marginally smaller graph.
  • Upgrade cost: explicit ownership makes compatibility testing targeted; accidental transitives make upgrades cascade unpredictably.

Knowledge check

Your source imports a transitive dependency’s API. Is “it already arrives through another library” a good reason not to declare it?

You need the same dependency but at one governed version across many paths. Should you exclude it everywhere?

When is provided correct?

Why can a separate feature module be clearer than optional=true?

Why is system scope risky for CI portability?

Summary

Choose Maven dependency controls according to ownership. Scope says where a dependency is required; direct declarations record source/API ownership; exclusions remove specific transitive paths; version management controls selected versions without erasing valid paths; optional dependencies shift feature dependency responsibility to consumers; and system scope creates machine coupling. The best POM is not the shortest POM—it is the one whose graph matches the real build and deployment contract.

Next lesson

Diagnose broken classpaths without guesswork

Lesson 4 intentionally breaks provided/optional/exclusion assumptions and applies the Academy diagnostic sequence from evidence preservation through controlled rebuild.

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.