skip to content

How does Maven decide the effective scope of a transitive dependency, and why does compile -> provided become none?

level: seniorimportance: should knowfreq 45%

answer

  1. direct scope (row) x transitive scope (column) -> effective
  2. compile + provided = none
  3. compile + runtime = runtime
  4. provided/system/test never propagate as shippable
  5. redeclare directly to override (nearest-wins)

basics

~20 s

Maven narrows a transitive dependency's scope based on the scope of the direct dependency that pulled it in. A transitive provided (or system) dependency is dropped, so compile -> provided becomes none and you must redeclare it if you need it.

solid answer

~50 s

When your direct dependency A (with some scope) brings in B transitively, B's effective scope is the result of combining A's scope (the row) with B's declared scope in A's POM (the column), using Maven's scope-combination table. The rule of thumb: a transitive dependency is never broader than the path that introduced it, and provided/system transitives are not propagated at all. So compile + provided yields none — the provided dependency was a private compile-time concern of A, and the environment is expected to supply it, so Maven does not leak it to you. Likewise compile + runtime yields runtime, and anything brought by a test-scoped dependency stays test or is omitted. The practical consequence: if A needs a provided API to function and you also need it, you must declare it directly with the scope you want; relying on it transitively will fail at runtime or compile.

code

bash · 2 lines
bash
mvn dependency:tree -Dverbose
mvn dependency:tree -Dscope=runtime

go deeper

for a junior

Know that transitive dependencies can be dropped depending on scope.

for a middle

Read the combination table and predict simple cases like compile->runtime.

for a senior

Diagnose runtime ClassNotFound issues caused by compile->provided=none and fix by redeclaring.

for a principal

Codify dependency hygiene (BOMs, explicit redeclaration of provided APIs, dependency:analyze in CI) to prevent transitive surprises across many modules.

## Why narrowing exists If you depend on A, Maven also adds A's own dependencies (B, C, ...) so things work without you listing every jar. But blindly inheriting A's scopes would be wrong: A's *test* dependency should not pollute your runtime, and A's *provided* dependency is an environment promise that does not transfer. So Maven applies a **scope-combination table** that takes the **direct dependency's scope** and the **transitive dependency's declared scope** and produces the **effective scope** in your project. ## The combination table (direct row x transitive column) | direct \\ transitive | compile | provided | runtime | test | |---|---|---|---|---| | **compile** | compile | (none) | runtime | (none) | | **provided** | provided | (none) | provided | (none) | | **runtime** | runtime | (none) | runtime | (none) | | **test** | test | (none) | test | (none) | Key readings: - **compile -> provided = none.** A's provided dependency is supplied by *A's* runtime environment and is A's compile-time concern; Maven will not silently add it to your build. If you actually need it, declare it yourself. - **compile -> runtime = runtime.** It is not needed to compile your code either, only to run, so it stays runtime. - **provided -> compile = provided.** The whole subtree under a provided dependency is itself provided. - **test column and test row** never propagate as anything you ship; transitive test dependencies are dropped (none) so your test classpath stays clean. - **system** behaves like provided and is likewise not propagated. ## Practical implications 1. A transitive **provided** dependency disappearing is a common cause of `NoClassDefFoundError` at runtime when you assumed it would be there. 2. To force a different scope or version, **redeclare the dependency directly** — a direct declaration always wins over a transitive one (nearest-wins) and lets you set the scope explicitly. 3. Inspect actual resolution with `mvn dependency:tree -Dscope=runtime` or just `mvn dependency:tree` and read the scope annotations. ```bash # See the resolved tree with scopes; verbose shows omitted/conflicted nodes mvn dependency:tree -Dverbose # Limit to a single scope's view mvn dependency:tree -Dscope=compile ``` ## Mental model Think of each transitive node as inheriting the *minimum reach* of the path above it. provided/system are 'borrowed' jars that never travel; test is 'private to A's tests'. Only compile and runtime genuinely flow downstream.

  • Your app throws NoClassDefFoundError for a class that lived in a transitive provided dependency. Why and how do you fix it?
    compile -> provided narrows to none, so the jar was never on your runtime classpath. Declare that dependency directly in your POM with the scope you actually need (compile or runtime).
  • Do transitive test dependencies appear on your test classpath?
    No. A's test dependencies are private to A; they are dropped (effective scope none) so your build only gets your own declared test dependencies.
  • How do you override the scope or version of a transitive dependency?
    Declare it directly (nearest-wins) or pin it via <dependencyManagement>, optionally with <exclusions> on the path that brought the unwanted one.

saying these in an interview costs you the question

  • Claiming transitive dependencies keep their original declared scope unchanged.
  • Assuming a provided API will be available transitively in your app.
  • Confusing scope narrowing with version mediation (nearest-wins) — they are separate mechanisms.

context