What is dependency substitution in Gradle, and what problem does it solve?
answer
- rewrite A → B during resolution
- module() and project() selectors
- affects transitives too
- can change identity, not just version
- foundation of composite builds
basics
~20 sDependency substitution lets you swap one dependency for another during resolution — for example, replacing a published external module with a local project so you build and test against your own source instead of a release.
solid answer
~40 sDependency substitution is a resolution rule that rewrites a requested dependency to a different one while Gradle builds the dependency graph. You configure it inside `configurations.all { resolutionStrategy.dependencySubstitution { ... } }` using `substitute(module("group:name")).using(project(":path"))` (or the reverse, project→module). The most common use is replacing a published external module with a local project — e.g. while developing a library you depend on — so you compile and test against live source instead of a released artifact. Substitution is transparent: it applies even to transitive occurrences of the module, so every place in the graph that pulls `com.acme:lib` gets your project instead. It differs from a plain `force` or version pin because it can change the *identity* (group/name) of a dependency, not just its version, and it integrates with composite builds where `includeBuild` performs substitution automatically.
code
kotlin · 7 linesconfigurations.all {
resolutionStrategy.dependencySubstitution {
substitute(module("com.acme:lib"))
.using(project(":lib"))
.because("build against local source during development")
}
}go deeper
Recall the core idea: swap one dependency for another, classic case being external module → local project for development.
Explain the DSL (substitute(module()).using(project())), that it affects transitives, and that it runs before conflict resolution.
Contrast with force/pin (identity vs version), mention .because() for traceability, and connect it to composite builds.
Frame substitution as a graph-rewrite primitive enabling org-wide source-vs-binary workflows and monorepo/composite strategies; discuss governance of who may add substitutions.
## What dependency substitution is When Gradle resolves a configuration (like `runtimeClasspath`), it builds a graph of every requested module and its transitives. **Dependency substitution** is a hook into that process: a rule that says "wherever you were about to use dependency A, use dependency B instead." The substitution happens during graph construction, so it affects direct *and* transitive references, and downstream conflict resolution then operates on the substituted node. ## Where you configure it It lives under a configuration's `resolutionStrategy`: ```kotlin configurations.all { resolutionStrategy.dependencySubstitution { substitute(module("com.acme:lib")) .using(project(":lib")) .because("develop against local source") } } ``` The two building blocks are: - `module("group:name")` or `module("group:name:version")` — a *component selector* for an external module. - `project(":path")` — a component selector for a local project. You can substitute in either direction: - **module → project**: replace a published artifact with a local project (the classic dev workflow). - **project → module**: replace a local project with a published artifact (e.g. to test against a release). ## Why not just change the version? A version pin or `force` keeps the same `group:name` and only chooses a version. Substitution can change the *identity* of the dependency — swap `com.acme:lib` for `:lib`, or even swap one module for a completely different module. That makes it strictly more powerful than version-only mechanisms, and it's the foundation that **composite builds** (`includeBuild`) build on: when you include another build, Gradle automatically substitutes any external module that matches an included project's published coordinates. ## Key properties - Applies to transitives, not just declared dependencies. - Runs before conflict resolution, so the substituted node participates in version selection. - Should carry a `.because("reason")` for traceability in the dependency report. - Substituting to a `project(...)` requires that project to expose matching variants/capabilities, or resolution can fail.
- Does substitution affect only direct dependencies or transitive ones too?Both — it rewrites the requested module wherever it appears in the graph, including transitive references, because it runs during graph construction before conflict resolution.
- How is substitution different from `force` or a version pin?force/pinning keep the same group:name and only choose a version; substitution can change the dependency's identity (group/name) or swap a module for a local project, so it's strictly more capable.
Like a phone-number redirect: callers dial the published number (the external module) but the call is silently routed to your desk (the local project) — they don't change what they dial, the routing changes underneath.
saying these in an interview costs you the question
- Claiming substitution only changes versions — it can change the whole module identity.
- Thinking it applies only to dependencies you declared directly, not transitives.
- Confusing it with `exclude`, which removes a dependency rather than replacing it.