How does module replacement differ from dependency substitution, and when would you choose one over the other?
answer
- replacement = identity, conflict-resolution only
- substitution = unconditional rewrite, can pin version
- substitution can swap module->project (composite)
- replacement never pins new version or introduces it
- renamed lib -> replacement; forced swap -> substitution
basics
~20 sSubstitution unconditionally swaps one selector for another module/project. Module replacement only states that two coordinates are the same logical library, so the old one is evicted during conflict resolution if both appear. Use replacement for renamed libraries, substitution for forced swaps.
solid answer
~40 sBoth can make `old` disappear in favour of `new`, but they operate differently. **Dependency substitution** (`configurations.all { resolutionStrategy.dependencySubstitution { substitute(module("g:old")).using(module("g:new:1.0")) } }`) is an unconditional, targeted rewrite: every request for `old` becomes a request for `new` (often a project, or a pinned version), whether or not both are in the graph. **Module replacement** (`dependencies.modules { module("g:old").replacedBy("g:new", "reason") }`) is a softer identity statement that only takes effect during conflict resolution when both are present — it evicts the old one but never pins the new one's version and never pulls the new one in by itself. Choose replacement to express the fact that a library was renamed/merged (google-collections → guava). Choose substitution when you must always route to a specific version, a local project (`includeBuild` composite), or patch a coordinate regardless of the graph.
code
kotlin · 17 lines// Replacement: states identity, evicts old when both present
dependencies {
modules {
module("com.google.collections:google-collections") {
replacedBy("com.google.guava:guava", "renamed")
}
}
}
// Substitution: unconditional, can pin version or swap to a project
configurations.all {
resolutionStrategy.dependencySubstitution {
substitute(module("com.google.collections:google-collections"))
.using(module("com.google.guava:guava:33.0.0-jre"))
.because("force migration")
}
}go deeper
Know that both can make an old module go away, and that replacement is the one for renamed libraries.
State that substitution is unconditional and can pin a version, while replacement only fires on conflict and doesn't pin.
Map each to its DSL location and explain the identity-vs-rewrite distinction, including project substitution for composites.
Advise teams on which to standardise: replacement rules for coordinate renames in a platform plugin; substitution reserved for deliberate, audited overrides.
## Two tools, overlapping outcomes Gradle offers several ways to make a module change identity during resolution. Two commonly confused ones are **module replacement** and **dependency substitution**. ## Module replacement ```kotlin dependencies { modules { module("com.google.collections:google-collections") { replacedBy("com.google.guava:guava", "renamed/merged") } } } ``` - A statement of **identity**: old and new are the *same logical component*. - Only acts during **conflict resolution** — when both old and new are present, the old is evicted. - Does **not** introduce the new module, and does **not** pin its version. - Lives in `dependencies { modules { ... } }`, project-scoped. ## Dependency substitution ```kotlin configurations.all { resolutionStrategy.dependencySubstitution { substitute(module("com.google.collections:google-collections")) .using(module("com.google.guava:guava:33.0.0-jre")) .because("renamed") } } ``` - An **unconditional rewrite** of a selector. Every request for `old` becomes `new`. - Acts whether or not the target is already in the graph — it can *introduce* the replacement and *pin its version*. - Can also substitute a module with a **project** (`project(":lib")`) — the basis of composite builds (`includeBuild`). - Lives in `resolutionStrategy.dependencySubstitution`, can be applied per-configuration. ## Decision guide | Need | Use | |------|-----| | Express that a library was renamed/merged; let Gradle dedupe when both appear | **Module replacement** | | Always force a particular version or coordinate, even if old isn't in conflict | **Substitution** | | Swap a published module for a local project / composite build | **Substitution (with project(...))** | | Keep the new module's version under normal resolution | **Module replacement** | ## A subtlety Replacement is closer in spirit to capabilities (declaring identity) than to a brute-force swap. If your goal is purely "these are the same library, dedupe them," replacement is the more semantically correct, lower-blast-radius choice. If your goal is "I need to control exactly which artifact resolves," substitution gives you that control.
- Which mechanism can replace a published dependency with a local project in a composite build?Dependency substitution (`substitute(...).using(project(...))`), which is what `includeBuild` uses under the hood. Module replacement cannot target a project — it only relates two external coordinates.
- If you want to both rename and force a specific version, which do you reach for?Substitution, because it can pin the target version (`module("g:new:1.2")`). Replacement deliberately leaves the new module's version to normal resolution.
saying these in an interview costs you the question
- Treating the two as interchangeable — substitution is unconditional and version-aware; replacement is conflict-resolution-only and version-agnostic.
- Claiming module replacement can swap a module for a local project.