skip to content

What does includeBuild in the settings script do, and how does a composite build differ from a multi-project build?

level: seniorimportance: should knowfreq 38%

answer

  1. include = subproject of one build
  2. includeBuild = compose independent builds
  3. dependency substitution by coordinate
  4. edit library + consumer, no publish
  5. each composite build keeps its own settings

basics

~20 s

includeBuild("../lib") pulls a separate, standalone Gradle build into the current one as a composite build. Unlike include() (which adds a subproject to one build), it wires together independent builds, substituting external dependencies with the included build's outputs.

solid answer

~40 s

`include(":app")` adds a *subproject* to a single build — all subprojects share one settings script and one root. `includeBuild("../shared-lib")` instead composes two *independent* builds: each has its own settings script and could be built on its own. Gradle treats the included build's published artifacts as candidates for **dependency substitution**: if your build depends on `com.acme:shared-lib:1.0` and the included build produces that coordinate, Gradle transparently substitutes the local source build for the binary, so you can edit and rebuild the library and the consumer together without publishing. This is ideal for working across repo boundaries — developing a library and its consumer simultaneously, or wiring a plugin build into the build that uses it (`includeBuild` of a `buildSrc`-style convention plugin). Substitutions can be explicit via `dependencySubstitution` when coordinates don't match by default.

code

kotlin · 11 lines
kotlin
// consumer build's settings.gradle.kts
rootProject.name = "consumer"

// compose a separate, standalone build living in a sibling directory
includeBuild("../shared-lib") {
    dependencySubstitution {
        // map the binary coordinate to the included build's source
        substitute(module("com.acme:shared-lib")).using(project(":"))
    }
}
// now implementation("com.acme:shared-lib:1.0") is built from ../shared-lib

go deeper

for a junior

Awareness that includeBuild exists and pulls in another build is enough.

for a middle

Contrast include vs includeBuild and name dependency substitution as the mechanism.

for a senior

Explain substitution-by-coordinate, the cross-repo/plugin-development use cases, and explicit dependencySubstitution.

for a principal

Position composite builds in a monorepo-vs-polyrepo strategy: when to compose builds for local dev vs publish artifacts, and the governance of substitution rules.

## Two different aggregation mechanisms Gradle offers two ways to combine code, and they operate at different levels: - **`include(":app")` — multi-project build.** Adds a *project* to the **same** build. There is one settings script, one root project, one shared configuration/lifecycle. Subprojects reference each other by path: `implementation(project(":core"))`. - **`includeBuild("../lib")` — composite build.** Pulls a *whole separate build* (which has its own `settings.gradle`) into the current one. The included build remains independently buildable; it is *composed* in, not absorbed. ## What includeBuild actually does ```kotlin // settings.gradle.kts of the consumer build rootProject.name = "consumer" includeBuild("../shared-lib") ``` With this, Gradle performs **dependency substitution**: any external (binary) dependency on a module coordinate that the included build *produces* is automatically replaced by the included build's project output. So if `consumer` declares `implementation("com.acme:shared-lib:1.0")` and `../shared-lib` publishes `com.acme:shared-lib`, Gradle builds `shared-lib` from source and links it in — no `publishToMavenLocal`, no version bump, no round trip. ## Why this is powerful - **Cross-repo development:** edit the library and its consumer in one IDE session, run the consumer's tests against your in-flight library change instantly. - **Plugin development:** `includeBuild("../my-gradle-plugin")` lets the consuming build use the plugin straight from source. - **Breaking up monoliths:** split a giant multi-project build into several composable builds without losing the ability to build the whole thing together. ## Explicit substitution When the coordinates don't line up (e.g. group differs), declare it: ```kotlin includeBuild("../shared-lib") { dependencySubstitution { substitute(module("com.acme:shared-lib")).using(project(":")) } } ``` ## Key contrasts to state in an interview | | `include` (multi-project) | `includeBuild` (composite) | |---|---|---| | Settings scripts | one, shared | each build keeps its own | | Reference | `project(":core")` | dependency substitution by coordinate | | Independence | subproject can't build alone | included build is standalone | | Typical use | modules of one product | cross-repo / plugin / source dependency | ## Gotchas - An included build cannot itself include the build that includes it (no cycles). - Substitution only triggers when a declared external dependency *matches* a produced coordinate (or you wire it explicitly). - Task addressing reaches into included builds with a leading build name path, e.g. `gradle :shared-lib:build` is run via the included build's own tasks; you orchestrate cross-build task deps with `dependsOn(gradle.includedBuild("shared-lib").task(":build"))`.

  • How does Gradle know to use the included build instead of the binary dependency?
    Via dependency substitution: it matches declared external coordinates against the module coordinates the included build produces. On a match it substitutes the local source build. If coordinates don't match, you declare substitutions explicitly in the includeBuild block.
  • Can a composite build replace publishToMavenLocal for local library development?
    Yes — that's a primary use case. includeBuild composes the library from source and substitutes it for the binary dependency, so you skip the publish-to-local-then-consume round trip and get instant feedback across both builds.
  • How do you make a consumer task depend on a task in an included build?
    Use gradle.includedBuild("shared-lib").task(":someTask") inside a dependsOn, since the included build has its own task namespace rather than being addressable as a plain :path within the root build.

include() is adding rooms to one house under a single blueprint; includeBuild() is connecting two complete houses with a hallway — each still stands on its own.

saying these in an interview costs you the question

  • Treating includeBuild and include as interchangeable — they aggregate at different levels.
  • Claiming includeBuild merges the two builds into one settings script — each keeps its own.
  • Forgetting that substitution requires matching (or explicitly mapped) coordinates.

context