skip to content

A teammate added includeBuild for a library but the consumer still downloads the old published jar from the repo. How do you diagnose and fix it?

level: seniorimportance: should knowfreq 40%

answer

  1. silent fallback to repo on coord mismatch
  2. dependencyInsight --dependency to verify
  3. look for -> project annotation
  4. align group or add explicit rule
  5. no warning when includeBuild unused

basics

~10 s

Substitution silently fell back to the repo because the included build's group:name doesn't match the requested coordinates. Verify with the dependencies report, then align coordinates or add an explicit substitute(module).using(project) rule.

solid answer

~40 s

Composite substitution is **silent** when it fails: if the included build doesn't publish coordinates matching the consumer's request, Gradle just resolves from the repository with no warning. Diagnose by running `./gradlew :app:dependencyInsight --dependency com.acme:widgets` — if it shows the version coordinate instead of `-> project :widgets`, substitution didn't fire. Common causes: the included build's (or its subproject's) `group` differs from what the consumer declares; the published `name`/`archivesName` differs from the project name; or the `includeBuild` path is wrong. Fix by aligning the included build's `group` to the requested coordinate, or — without touching the library — add an explicit `dependencySubstitution { substitute(module("com.acme:widgets")).using(project(":widgets")) }` to the `includeBuild` block. Then re-run the report to confirm `-> project`.

code

bash · 10 lines
bash
# Did substitution fire? Look for '-> project :widgets'
./gradlew :app:dependencyInsight --dependency com.acme:widgets

# If not, fix without editing the library: add an explicit rule
# in settings.gradle.kts:
#   includeBuild("../widgets-lib") {
#     dependencySubstitution {
#       substitute(module("com.acme:widgets")).using(project(":widgets"))
#     }
#   }

go deeper

for a junior

Recognize that the jar still comes from the repo and something is wrong, even if you can't fully diagnose.

for a middle

Use the dependencies/dependencyInsight report to confirm whether -> project appears.

for a senior

Name the root cause (coordinate mismatch / silent fallback) and apply the explicit-rule or align-group fix.

for a principal

Establish a convention that all internal libraries share consistent group coordinates so composites 'just work', plus CI checks that don't rely on includeBuild.

## Symptom You ran `includeBuild("../widgets-lib")` expecting your local edits to take effect, but the build still uses the stale published jar. Nothing errors — that's the trap. ## Why composites fail silently Automatic substitution requires the included build to **publish coordinates equal to** what the consumer requests (by `group:name`). If they differ by even one character, Gradle finds no match and falls through to normal repository resolution. There is **no warning** for an unused included build in this case. ## Step-by-step diagnosis 1. **Inspect resolution.** Run: ```bash ./gradlew :app:dependencyInsight --dependency com.acme:widgets ``` - `-> project :widgets` → substitution worked. - shows `com.acme:widgets:1.4.0` only → it did **not**. 2. **Check the included build's coordinates.** Look at the library's `group` (in its root or subproject `build.gradle.kts`) and its `base.archivesName`/project name. These together form the published `group:name`. 3. **Check the includeBuild path** is correct and the build actually configures (look for it in `./gradlew :app:buildEnvironment` / the configuration output). 4. **Mind the `--offline` / cache** — a previously cached jar can mask intent, but substitution overrides cache when it fires, so a cached jar means it didn't fire. ## Fixes - **Align coordinates** (preferred long-term): set the library's `group` to match the requested one so automatic substitution works for everyone. - **Explicit rule** (no library change): ```kotlin includeBuild("../widgets-lib") { dependencySubstitution { substitute(module("com.acme:widgets")).using(project(":widgets")) } } ``` - Re-run `dependencyInsight` to confirm `-> project`. ## Gotchas - A self-included build (the build can't substitute its own published module into itself) won't substitute. - Substituting a module that the included build itself depends on transitively can create loops — keep mappings minimal and explicit.

  • Why doesn't Gradle warn you when an included build's substitution never matches?
    Automatic substitution is best-effort by design; a non-matching included build is treated as simply not relevant to that dependency, so resolution falls through to repositories without error.
  • What single command tells you whether substitution happened?
    `./gradlew :app:dependencyInsight --dependency <group:name>` — a `-> project :x` annotation confirms substitution; a bare version coordinate means it didn't.

saying these in an interview costs you the question

  • Assuming a missing error means substitution worked — it fails silently.
  • Blaming the dependency cache when the real issue is mismatched group:name.
  • Editing build.gradle dependency versions to 'force' the local build instead of fixing coordinates/substitution.

context