How does dependency substitution differ from module replacement, exclusion, and forcing — and when is each appropriate?
answer
- substitution = redirect (can hit a project)
- replacement = declare equivalence for conflicts
- exclude = remove transitive
- force/strict = pick a version only
- match tool to intent
basics
~20 sSubstitution redirects a dependency to a different one (including a local project) during resolution. Replacement signals two modules are the same library under different coordinates. Exclude removes a transitive. Force/pin only chooses a version. Pick by intent.
solid answer
~40 sThese mechanisms overlap superficially but serve different intents. **Substitution** (`substitute(module()).using(project()/module())`) rewrites a dependency to another component — different version, different coordinates, or a local project — and is the only one that can point at a `project(...)`; it's your tool for source-vs-binary dev and composite builds. **Module replacement** (`modules { module("a:b").replacedBy("c:d") }`) is a *conflict-resolution* declaration: it tells Gradle two coordinates are the same library (e.g. `com.google.collections:google-collections` replaced by `com.google.guava:guava`) so only one survives, but it doesn't redirect a working dependency the way substitution does. **Exclude** removes a transitive from the graph entirely. **Force/strict** only selects a version of the same module. So: redirect to a project or different module → substitution; declare equivalence of two libraries → replacement; drop an unwanted transitive → exclude; pin a version → force/strict constraint.
code
kotlin · 6 lines// Redirect to a local project — substitution
configurations.all {
resolutionStrategy.dependencySubstitution {
substitute(module("com.acme:lib")).using(project(":lib"))
}
}go deeper
Know substitution swaps a dependency, exclude removes one; rough sense of the difference.
Clearly map each tool to its intent and know only substitution can target a project.
Explain interaction with conflict resolution (replacement is conflict metadata, substitution rewrites the node pre-conflict) and choose deliberately.
Set team conventions for which mechanism to use, keeping dependency reports explainable and avoiding overlapping/contradictory rules across builds.
## Four tools, four intents It's easy to reach for the wrong knob. Here's the decision map, focused on what makes substitution distinct. ### Dependency substitution - **DSL**: `resolutionStrategy.dependencySubstitution { substitute(module("g:n")).using(project(":p")) }` (or `.using(module("g2:n2:v"))`). - **What it does**: rewrites the requested component to a different one during graph construction — can change version, change identity, or point to a *local project*. - **Unique power**: only substitution can target `project(...)`; it underpins composite builds. - **Use when**: developing against local source, swapping a fork in, redirecting a renamed artifact to a project. ### Module replacement - **DSL**: `dependencies { modules { module("a:b") { replacedBy("c:d", "reason") } } }`. - **What it does**: declares that two *different* modules are the same logical library, so Gradle treats them as conflicting and keeps one. This is conflict metadata, not a redirect of a healthy dependency. - **Use when**: a library was renamed/relocated (google-collections → guava) and both might appear transitively. ### Exclude - **DSL**: `implementation("g:n") { exclude(group = "x", module = "y") }` or configuration-level. - **What it does**: removes a transitive dependency from the graph. No replacement happens. - **Use when**: you don't want a transitive at all (and provide it another way or genuinely don't need it). ### Force / strict constraint - **DSL**: `resolutionStrategy.force("g:n:1.0")` or a constraint with `version { strictly("1.0") }`. - **What it does**: chooses a specific version of the *same* module; cannot change identity or point to a project. - **Use when**: you need to pin a version across the graph. ## The crisp distinctions - Only **substitution** can redirect to a `project(...)`. - **Replacement** is about declaring equivalence for conflict resolution, not rerouting a present-and-correct dependency. - **Exclude** subtracts; substitution swaps; force selects. ```kotlin // substitution: redirect to local project resolutionStrategy.dependencySubstitution { substitute(module("com.acme:lib")).using(project(":lib")) } // replacement: declare equivalence (conflict metadata) dependencies.modules { module("com.google.collections:google-collections") { replacedBy("com.google.guava:guava", "renamed library") } } ``` Knowing which intent you have keeps build logic honest and the dependency report explainable.
- Which of these mechanisms can point a dependency at a local project?Only dependency substitution — via `substitute(module(...)).using(project(...))`. Replacement, exclude, and force cannot target a project.
- When would module replacement be the right tool instead of substitution?When two different coordinates represent the same library (e.g. a renamed/relocated artifact) and you want Gradle to treat them as a conflict and keep one — that's equivalence metadata, not a redirect.
saying these in an interview costs you the question
- Using exclude when you actually want to redirect to a different module — exclude removes, it doesn't swap.
- Treating replacedBy as a way to develop against local source — it can't target a project.
- Thinking force can change a module's group/name — it only selects a version.