skip to content

In a Gradle composite build, what happens to a dependency on a published module when you include the producing build with includeBuild?

level: juniorimportance: must knowfreq 60%

answer

  1. includeBuild = automatic substitution
  2. match by group:name, version ignored
  3. external module -> local project
  4. no publish needed
  5. silent fallback if coords mismatch

basics

~10 s

Gradle automatically substitutes the published module coordinates with the local included build's project. Instead of downloading the artifact from a repository, it builds and uses the local source.

solid answer

~40 s

When you `includeBuild("../lib")`, Gradle inspects the included build's projects and the `group:name` coordinates they publish. For any dependency in the consuming build that matches those coordinates, Gradle performs **automatic dependency substitution**: it replaces the external module with the local project, so the artifact is built from source rather than fetched from a repository. This lets you develop a library and its consumer together without publishing intermediate snapshots. The version you declared is ignored for matching — substitution is by `group` and `name` only. If the coordinates don't match (e.g. a typo in the included build's `group`), no substitution happens and Gradle silently falls back to the repository, which is a common source of confusion.

code

kotlin · 9 lines
kotlin
// settings.gradle.kts of the consuming build
rootProject.name = "app"
includeBuild("../widgets-lib") // local build that publishes com.acme:widgets

// build.gradle.kts of :app
dependencies {
    // resolves to the local widgets project, version is ignored for matching
    implementation("com.acme:widgets:1.4.0")
}

go deeper

for a junior

State that includeBuild makes Gradle build the dependency from local source instead of downloading it.

for a middle

Explain matching is by group:name with version ignored, and that it's automatic for included builds.

for a senior

Discuss the silent-fallback failure mode and how to verify substitution with the dependencies report.

for a principal

Frame composites + substitution as a workflow for cross-repo development without snapshot publishing, and its CI implications.

## What a composite build is A **composite build** (a.k.a. *included build*) is one Gradle build that pulls in one or more other, fully independent Gradle builds via `includeBuild`. Each included build keeps its own `settings.gradle(.kts)`, its own root, and its own version — it is not a subproject. The mechanism that makes composites useful is **dependency substitution**. ## Automatic substitution When you add `includeBuild("../my-lib")` to the consuming build's `settings.gradle.kts`, Gradle: 1. Configures the included build and enumerates every project it contains. 2. Determines the **publishing coordinates** (`group:name`) of each of those projects. 3. Builds a substitution map: `group:name` → local project. 4. While resolving the consumer's dependencies, any external module dependency whose `group:name` matches an entry is **replaced** by the corresponding local project's outputs. The key point: matching is done on **group and name only — the version is ignored**. So `implementation("com.acme:widgets:1.4.0")` resolves to the local `widgets` project even though the local version is `2.0-SNAPSHOT`. ## Why it matters This means you can clone a library next to your app, point at it with `includeBuild`, and immediately build against your local edits — no `publishToMavenLocal`, no version bumps, no stale snapshot caches. Change library source, rebuild the app, see the change. ## The silent-fallback trap If the included build's `group` (or a subproject's `group`) does not exactly match the coordinates the consumer declares, **no substitution occurs and there is no error** — Gradle just resolves from the repository. This is the most common reason a composite "doesn't work". Run `./gradlew :app:dependencies` and look for `-> project :lib` annotations to confirm substitution actually happened. ```kotlin // settings.gradle.kts (consumer) includeBuild("../widgets-lib") ```

  • How can you confirm substitution actually happened rather than a repository resolve?
    Run `./gradlew :app:dependencies` (or `dependencyInsight`) and look for `-> project :widgets` next to the module; if it still shows the version coordinate, substitution did not occur.
  • Does the declared version of the dependency matter for automatic substitution?
    No. Automatic matching uses only `group:name`; the requested version is ignored once the included build publishes those coordinates.

Like a symlink for dependencies: the coordinate name still points where it always did, but Gradle quietly redirects it to your working copy on disk instead of the warehouse.

saying these in an interview costs you the question

  • Claiming you must publish to mavenLocal first — that defeats the whole point of a composite build.
  • Believing the version must match between consumer and included build.

context