Can dependency substitution change a dependency's version or swap it for a different external module? Show how and explain the resolution implications.
answer
- using(module()) for version or module swap
- swap identity, not just version
- substitute runs BEFORE conflict resolution
- substituted version can still be upgraded
- dependencyInsight to verify
basics
~10 sYes. 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 sSubstitution 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 linesresolutionStrategy.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
Know substitution can also change a version or swap to another module, not just point to a project.
Explain the version/module swap DSL and that substitution runs before conflict resolution.
Reason about substitution+conflict interplay, conditional substitution via dependencySubstitution.all, and verification with dependencyInsight.
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.