skip to content

Can dependency substitution change a dependency's version or swap it for a different external module? Show how and explain the resolution implications.

level: middleimportance: should knowfreq 26%

answer

  1. using(module()) for version or module swap
  2. swap identity, not just version
  3. substitute runs BEFORE conflict resolution
  4. substituted version can still be upgraded
  5. dependencyInsight to verify

basics

~10 s

Yes. substitute(module("g:n")).using(module("g:n:1.5")) changes the version; substitute(module("g:n")).using(module("g2:n2:1.0")) swaps to a different module. The substituted node then participates in normal conflict resolution.

solid answer

~50 s

Substitution isn't limited to project redirects. You can substitute one external module for another version of itself, or for an entirely different module. `substitute(module("com.acme:lib")).using(module("com.acme:lib:1.5"))` rewrites every reference to a chosen version — broader than `force` because it can be conditional and carry a `.because(...)`. `substitute(module("old:lib")).using(module("new:lib:2.0"))` swaps to a different coordinate, useful for redirecting to a fork or a maintained drop-in. Crucially, substitution runs *before* conflict resolution: the substituted node enters the graph as if it were originally requested, so it then competes in normal version selection with other requests for the same module. That means a substitution to version 1.5 can still be upgraded to 1.7 if another path demands 1.7 (unless you also pin). Understanding this ordering — substitute first, then resolve conflicts — prevents surprise when a substituted version isn't the final one.

code

kotlin · 8 lines
kotlin
resolutionStrategy.dependencySubstitution {
    // change version
    substitute(module("com.acme:lib"))
        .using(module("com.acme:lib:1.5")).because("known-good")
    // swap to a different module
    substitute(module("org.old:lib"))
        .using(module("org.fork:lib:2.0")).because("upstream unmaintained")
}

go deeper

for a junior

Know substitution can also change a version or swap to another module, not just point to a project.

for a middle

Explain the version/module swap DSL and that substitution runs before conflict resolution.

for a senior

Reason about substitution+conflict interplay, conditional substitution via dependencySubstitution.all, and verification with dependencyInsight.

for a principal

Govern when fork-swaps are acceptable, ensure reasons are recorded, and keep substitution rules from silently masking version-management policy.

## Substitution targets beyond projects The `using(...)` side accepts any component selector, so substitution can: 1. **Change version** of the same module: ```kotlin resolutionStrategy.dependencySubstitution { substitute(module("com.acme:lib")) .using(module("com.acme:lib:1.5")) .because("known-good build") } ``` 2. **Swap to a different module** (fork / drop-in replacement): ```kotlin substitute(module("org.old:lib")) .using(module("org.fork:lib:2.0")) .because("upstream unmaintained") ``` ## The critical ordering: substitute, then resolve conflicts Dependency resolution proceeds roughly as: build the requested graph → apply substitutions (rewriting nodes) → run conflict resolution to pick one version per module → select variants/artifacts. Because substitution happens **before** conflict resolution, the substituted node is treated like an original request and **participates in version selection**. Practical consequence: if you substitute `com.acme:lib` to `1.5`, but another dependency transitively requests `com.acme:lib:1.7`, default conflict resolution picks the *highest*, so you may end up on `1.7` — your substitution chose an *input* to selection, not the final answer. If you need `1.5` to win unconditionally you combine substitution with a strict constraint or `force` (those are separate mechanisms covered by sibling topics). ## Why use substitution-to-module over force? - It can change **identity** (group/name), which `force` cannot. - It can be made **conditional** by inspecting `dependencySubstitution.all { ... }` and the requested selector, applying only to certain requests. - It carries a `.because(...)` reason that shows up in the dependency insight report. ```bash ./gradlew dependencyInsight --configuration runtimeClasspath --dependency com.acme:lib ``` The insight report shows the substitution reason and the selection outcome, making it easy to verify whether your substituted version actually won. ## Summary - `using(module("g:n:v"))` changes version; `using(module("g2:n2:v"))` changes the whole module. - Substitution feeds conflict resolution; it does not bypass it. - Combine with strict/force when you need the substituted version to be final.

  • If I substitute lib to 1.5 but a transitive needs 1.7, which version is used?
    By default the highest, so 1.7 — because substitution runs before conflict resolution and the substituted node only feeds selection. To force 1.5 you'd add a strict constraint or force.
  • How can you verify a substitution actually took effect?
    Run `./gradlew dependencyInsight --dependency g:n`; the report shows the substitution reason (`because`) and the final selected version.
  • Why pick substitution-to-module over a plain version force?
    Substitution can change the module identity (group/name), can be applied conditionally, and records a traceable reason — force only selects a version of the same module.

saying these in an interview costs you the question

  • Believing a substituted version is always the final resolved version — conflict resolution can still override it.
  • Using substitution where a simple version constraint suffices, adding needless complexity.
  • Forgetting `.because(...)`, making the dependency report hard to audit.

context