How does composite-build dependency substitution via includeBuild differ from substitution declared with resolutionStrategy.dependencySubstitution?
answer
- same DSL, different scope
- composite = settings, cross-build, project in included build
- resolutionStrategy = build, per-configuration, intra-build
- composite is automatic; resolutionStrategy is explicit
- resolutionStrategy supports .because()
basics
~10 sComposite substitution lives on the includeBuild call in settings and maps a module to a project in another, included build. resolutionStrategy.dependencySubstitution is configuration-level and substitutes within the same build (module-to-module or module-to-local-project).
solid answer
~40 sBoth use the same `substitute(...).using(...)` grammar, but they operate at different scopes. **Composite substitution** is declared in `settings.gradle.kts` as a `dependencySubstitution { }` block on a specific `includeBuild(...)`; its `project(...)` targets refer to projects **inside that included (separate) build**, enabling cross-build local development. **`resolutionStrategy.dependencySubstitution`** is declared per `Configuration` (or via `configurations.all`) in `build.gradle.kts`; it operates **within the current build** and is typically used to redirect one module to another module (e.g. swap `commons-logging` for `slf4j`), pin versions, or point at a subproject of the same build. Roughly: includeBuild substitution = automatic + cross-build composite wiring; resolutionStrategy substitution = explicit, intra-build dependency surgery applied during configuration resolution. They can coexist.
code
kotlin · 15 lines// COMPOSITE (settings.gradle.kts) — cross-build, into an included build
includeBuild("../widgets-lib") {
dependencySubstitution {
substitute(module("com.acme:widgets")).using(project(":widgets"))
}
}
// resolutionStrategy (build.gradle.kts) — intra-build module swap
configurations.all {
resolutionStrategy.dependencySubstitution {
substitute(module("commons-logging:commons-logging"))
.using(module("org.slf4j:jcl-over-slf4j:2.0.13"))
.because("route legacy logging through slf4j")
}
}go deeper
Know that there are two places substitution can be configured; don't need the full distinction.
Place composite substitution in settings on includeBuild, resolutionStrategy in build.gradle per configuration.
Articulate the scope difference (cross-build vs intra-build) and pick the right one per use case.
Define team conventions for when to use composites vs resolutionStrategy swaps, including reproducibility and CI concerns.
## Same verbs, different scope Gradle reuses the `DependencySubstitution` DSL (`substitute(module(...)).using(...)`) in two places. Knowing which one you're in is the whole question. ## Composite substitution (settings, cross-build) Declared on an `includeBuild` in `settings.gradle.kts`: ```kotlin includeBuild("../widgets-lib") { dependencySubstitution { substitute(module("com.acme:widgets")).using(project(":widgets")) } } ``` - Targets a project **in the included build** (a different, standalone Gradle build). - Happens **automatically** for matching coordinates even without the block; the block is for overrides. - Purpose: develop a library and its consumer together without publishing. ## resolutionStrategy substitution (build, intra-build) Declared per configuration in `build.gradle.kts`: ```kotlin configurations.all { resolutionStrategy.dependencySubstitution { substitute(module("org.example:old-lib")) .using(module("org.example:new-lib:2.0")) .because("old-lib is EOL") } } ``` - Operates **within the current build** during resolution of that configuration. - Commonly maps **module → module** (swap or pin), or **module → project(":sub")** for projects of the *same* build. - Supports `.because(reason)` for traceable rationale. - Never crosses into a separate included build (that's the settings-level job). ## Decision guide - Cross-repo / cross-build local dev → composite substitution (includeBuild). - Swap/pin/redirect a dependency in one build, give a reason, apply selectively per configuration → resolutionStrategy. ## Note on ownership The general `resolutionStrategy.dependencySubstitution` API is a separate topic; here the focus is the composite (includeBuild) flavour. The takeaway is recognizing the boundary: settings-level/included-build vs configuration-level/same-build.
- Can resolutionStrategy substitution point at a project in a separate included build?No. Its `project(...)` targets refer to projects of the same build. Cross-build mapping into an included build is the job of the settings-level dependencySubstitution on includeBuild.
- Which form supports a `.because(...)` rationale?The resolutionStrategy.dependencySubstitution form; it's used to record why a swap/pin was applied for traceability in reports.
saying these in an interview costs you the question
- Saying composite substitution is configured in build.gradle — it's on includeBuild in settings.
- Claiming resolutionStrategy can reach into a separate included build.
- Treating the two as identical because they share the substitute/using DSL.