skip to content

A teammate forced a transitive library to an older version and now a runtime NoSuchMethodError appears. How do you diagnose what won resolution, and why might `force`/`strictly` be the wrong fix here?

level: seniorimportance: should knowfreq 38%

answer

  1. dependencyInsight --dependency
  2. selection reason: forced/by constraint/conflict
  3. downgrade ⇒ NoSuchMethodError at runtime
  4. force hides conflicts; strictly fails fast
  5. align up, not down

basics

~20 s

Run gradle dependencyInsight --dependency lib to see the selected version and reason. A forced downgrade can pull in an API too old for callers, causing NoSuchMethodError; the fix is usually to align upward or remove the override, not pin lower.

solid answer

~50 s

First, make the resolution observable: `./gradlew :app:dependencyInsight --dependency commons-lang3 --configuration runtimeClasspath` shows the *selected* version, *why* it won (forced / by constraint / by conflict resolution), and which paths requested what. `./gradlew dependencies` shows the full tree. A `force` or `strictly` that **downgrades** a transitive can break callers compiled against newer APIs — they invoke a method that doesn't exist in the older jar, hence `NoSuchMethodError`/`NoSuchMethodError` at runtime. Because `force` silently overrides, the failure surfaces far from its cause. The right fix depends on intent: if the pin was for a CVE, prefer a `strictly` constraint with `because(...)` so conflicts fail fast and are documented; if a caller genuinely needs the newer API, *raise* the pin or drop it and let conflict resolution pick the higher version. Forcing lower to 'win the conflict' without checking caller requirements is the anti-pattern.

code

bash · 3 lines
bash
./gradlew :app:dependencyInsight \
  --dependency org.apache.commons:commons-lang3 \
  --configuration runtimeClasspath

go deeper

for a junior

Know dependencyInsight shows the chosen version and run dependencies for the tree.

for a middle

Read the selection reason and connect a downgrade to a runtime NoSuchMethodError.

for a senior

Diagnose compile-vs-runtime divergence, replace force with documented strictly, and align upward.

for a principal

Set a policy: no silent force in shared modules; constraints+because in platforms, with insight checks gated in review/CI.

## Make resolution observable The two core tools: ```bash # Full resolved tree for a configuration ./gradlew :app:dependencies --configuration runtimeClasspath # Focused: why did THIS module resolve to THIS version? ./gradlew :app:dependencyInsight \ --dependency org.apache.commons:commons-lang3 \ --configuration runtimeClasspath ``` `dependencyInsight` is the key task: it prints the selected version, the **reason** (e.g. `Forced`, `By constraint`, `By conflict resolution: between versions …`), any `because(...)` text, and the request paths. Programmatically, the `ResolutionResult` API (`configuration.incoming.resolutionResult`) exposes the same selection reasons. ## Why a forced/strict downgrade breaks at runtime Gradle resolves **one** version of each module onto the classpath. If a caller was compiled against `commons-lang3:3.12` (which has method `X`) but a `force("…:3.5")` downgrades the runtime jar to `3.5` (no method `X`), compilation may still pass (if compileClasspath differs) but the JVM throws `NoSuchMethodError` when `X` is invoked. `force` makes this worse because it overrides silently — no conflict, no warning. ## Choosing the right remedy | Situation | Right fix | |---|---| | Pin was to dodge a CVE | `strictly` constraint + `because(...)`; let genuine conflicts fail fast | | A caller needs a newer API | Remove the downgrade or raise the pin upward | | Want to ban a single bad version | `reject("x.y.z")`, not a blanket force | | Org-wide alignment | Publish constraints in a platform/BOM | ## Why prefer `strictly` over `force` `force` resolves conflicts by fiat and hides incompatibilities. A `strictly` constraint participates in resolution and **fails loudly** when another module genuinely requires an incompatible version — turning a silent runtime `NoSuchMethodError` into a build-time error you can reason about. Pair it with `dependencyInsight` to confirm the new selection before committing. ## Practical workflow 1. Reproduce, capture the stack trace and the missing method. 2. `dependencyInsight` on the offending module to see selected version + reason. 3. Identify the caller's required version (its own metadata / compile target). 4. Replace the blunt `force` with a documented `strictly` (and `reject` for the bad version), aligning *up* if a caller needs it. 5. Re-run insight to verify; add a test exercising the path.

  • Why might the code still compile but fail at runtime after a forced downgrade?
    compileClasspath and runtimeClasspath are resolved independently; a force on one configuration (or differing constraints) can leave compile on the newer API while runtime gets the older jar, so the missing method only shows up when invoked.
  • How would you make the bad downgrade fail at build time instead?
    Replace force with a strictly constraint. If a caller strictly needs the newer version, the two strict requirements conflict and resolution fails with a clear message rather than producing a broken classpath.
  • What does the 'reason' field in dependencyInsight tell you?
    Why the version was selected — e.g. Forced, By constraint (with because text), By conflict resolution between specific versions, or rejected — pinpointing which mechanism decided the outcome.

saying these in an interview costs you the question

  • Reaching for force-lower to 'win the conflict' without checking caller API needs.
  • Assuming a clean compile means the classpath is correct at runtime.
  • Not using dependencyInsight and guessing at the selected version.

context