skip to content

What is dependency substitution in Gradle, and what problem does it solve?

level: juniorimportance: must knowfreq 55%

answer

  1. rewrite A → B during resolution
  2. module() and project() selectors
  3. affects transitives too
  4. can change identity, not just version
  5. foundation of composite builds

basics

~20 s

Dependency 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 s

Dependency 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 lines
kotlin
configurations.all {
    resolutionStrategy.dependencySubstitution {
        substitute(module("com.acme:lib"))
            .using(project(":lib"))
            .because("build against local source during development")
    }
}

go deeper

for a junior

Recall the core idea: swap one dependency for another, classic case being external module → local project for development.

for a middle

Explain the DSL (substitute(module()).using(project())), that it affects transitives, and that it runs before conflict resolution.

for a senior

Contrast with force/pin (identity vs version), mention .because() for traceability, and connect it to composite builds.

for a principal

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.

context