When you includeBuild a library, how does Gradle decide to use the local source instead of the published artifact?
answer
- match by group:name, not version
- coordinates are the join key
- version in declaration is ignored
- no match -> no substitution (build present but unused)
- wrong group = silent fallback to published artifact
basics
~20 sGradle matches the included build's published coordinates (group and module name) against external dependencies in the consumer. Where group:name matches, it substitutes the included build's output for the external artifact — the version is ignored for matching.
solid answer
~40 sWhen you `includeBuild("../lib")`, Gradle inspects what that build would publish — its `group` and the module `name` (typically `rootProject.name` / subproject names). It then scans the consuming build's dependency graph for external dependencies that share the same `group:name`. Any match is **substituted**: instead of resolving `com.example:my-library:1.0` from a repository, Gradle wires in the corresponding output of the included build. The **requested version is irrelevant** for matching — local source always wins while composed, which is exactly what you want for development. This is why your `dependencies { implementation("com.example:my-library:1.0") }` declaration doesn't change: the coordinates are the join key. If the included build's coordinates don't match anything, no substitution happens and the included build is simply present but unused by the consumer.
code
kotlin · 12 lines// my-library/build.gradle.kts
group = "com.example"
version = "2.0-SNAPSHOT" // version irrelevant for matching
// consumer/settings.gradle.kts
includeBuild("../my-library")
// consumer/app/build.gradle.kts
dependencies {
// substituted to local source because com.example:my-library matches
implementation("com.example:my-library:1.0")
}go deeper
Know that matching coordinates make Gradle use the local source instead of the published one.
Explain that group:name is the join key and the version is ignored.
Diagnose silent fallback when coordinates don't match and ensure group/name alignment.
Set conventions so included builds' coordinates reliably match consumer requests across the org, preventing confusing silent fallbacks.
## The matching rule Composite builds substitute by **coordinates**, not by version: - Gradle determines each included build's identity from its `group` and module `name` (derived from `rootProject.name` and any subprojects). - It then looks at the consumer's external dependencies. For every `group:name` that matches an included build's output, Gradle replaces the external dependency with the **project output of the included build**. - The **version in the dependency declaration is ignored** for the purpose of matching — the local source is preferred regardless of the requested version. ## Why this design During development you want the *current* local code, not whatever version string happens to be written. Ignoring the version means you don't have to keep editing dependency declarations to match the library's in-progress version. ```kotlin // my-library/build.gradle.kts group = "com.example" version = "2.0-SNAPSHOT" // rootProject.name = "my-library" -> coordinates com.example:my-library // consumer/build.gradle.kts dependencies { // matches com.example:my-library by group:name regardless of the 1.0 here implementation("com.example:my-library:1.0") } ``` While `includeBuild("../my-library")` is active, that `implementation` resolves to the local source. ## What happens with no match If the included build publishes `com.example:other` but the consumer never depends on those coordinates, nothing is substituted. The included build is still composed (its tasks are addressable) but it contributes no dependency to the consumer. ## Practical implications - Keep the included build's `group` and `name` aligned with the coordinates the consumer actually requests, or substitution silently won't happen. - A mismatch (e.g., wrong `group`) is the most common reason 'includeBuild doesn't pick up my changes' — Gradle resolved the published artifact because nothing matched. ## Note on scope Gradle can substitute automatically by coordinates as described here; explicit, manual substitution rules for non-matching cases are a related but distinct concern handled via dependency-substitution configuration.
- A teammate says includeBuild 'isn't working' — their changes don't show up. What do you check first?Whether the included build's group:name actually matches the dependency coordinates the consumer requests; a mismatch makes Gradle silently resolve the published artifact instead.
- Does the version in implementation("com.example:lib:1.0") affect whether substitution happens?No — matching is by group:name only; the local source wins regardless of the requested version while composed.
saying these in an interview costs you the question
- Saying the dependency version must match the included build's version for substitution — version is ignored for matching.
- Assuming includeBuild always overrides the dependency even when coordinates differ.