skip to content

How does a composite build with includeBuild use dependency substitution, and when would you reach for it?

level: middleimportance: must knowfreq 50%

answer

  1. includeBuild in settings
  2. auto-substitute by group:name match
  3. version ignored for matching
  4. no publishToMavenLocal step
  5. explicit dependencySubstitution block when coords differ

basics

~10 s

includeBuild wires a separate Gradle build into yours. Gradle automatically substitutes any external dependency whose coordinates match a project published by the included build, so you transparently build against that build's source.

solid answer

~40 s

A **composite build** combines independent Gradle builds. When you add `includeBuild("../other-lib")` in `settings.gradle.kts`, Gradle inspects the published coordinates (group:name) of the included build's projects and **automatically substitutes** any matching external module dependency in the consuming build with the included project. So if your app depends on `com.acme:lib:1.2` and the included build publishes `com.acme:lib`, your app builds and tests against the included source — no manual substitution rule needed. You reach for this when iterating across repo boundaries: fixing a bug in a library and immediately consuming it, or running an app against an unreleased dependency. If automatic matching isn't enough (coordinates differ), you can declare explicit substitutions via `includeBuild("../x") { dependencySubstitution { substitute(module("g:n")).using(project(":sub")) } }`. Composite builds avoid publishing to a local Maven repo as an intermediate step.

code

kotlin · 7 lines
kotlin
// settings.gradle.kts
includeBuild("../acme-lib") {
    dependencySubstitution {
        substitute(module("com.acme:lib"))
            .using(project(":lib"))
    }
}

go deeper

for a junior

Know that includeBuild wires another build in and you build against its source.

for a middle

Explain automatic group:name substitution, that version is ignored for matching, and the no-publish advantage.

for a senior

Discuss explicit substitution overrides, variant/capability compatibility requirements, and plugin-build inclusion.

for a principal

Position composite builds as a cross-repo/monorepo strategy; weigh against publishing pipelines, CI implications, and team workflow governance.

## Composite builds in one sentence A **composite build** is a build that includes other otherwise-independent Gradle builds so they resolve against each other's source. The mechanism that connects them is dependency substitution. ## Automatic substitution via includeBuild In `settings.gradle.kts`: ```kotlin rootProject.name = "app" includeBuild("../acme-lib") ``` When you include a build, Gradle reads the `group` and `name` (and `version` where relevant) that each project in the included build *would publish as*. Then, during the consuming build's resolution, any external dependency on a module whose coordinates match an included project is **automatically substituted** with that project. Concretely: `app` declares `implementation("com.acme:lib:1.2")`; the included `../acme-lib` build has a project that publishes `com.acme:lib`; Gradle silently routes the dependency to the included project. The requested version is ignored for matching purposes — coordinate (group:name) matching is what drives it. ## When the defaults aren't enough If the included project's coordinates don't match what the consumer asks for, you declare explicit substitutions scoped to that included build: ```kotlin includeBuild("../acme-lib") { dependencySubstitution { substitute(module("com.acme:legacy-name")) .using(project(":lib")) } } ``` This is the same `dependencySubstitution` DSL as in `resolutionStrategy`, but rooted at the included build. ## Why use it instead of publishToMavenLocal The old workflow was: change the library → `publishToMavenLocal` → rebuild the app picking up the snapshot. Composite builds remove the publish step entirely: the app sees the library's live source, with full incremental build, IDE navigation across both builds, and no stale local-repo artifacts. It's the go-to for cross-repo development and is heavily used in monorepo-ish setups. ## Caveats - The included project must expose compatible variants/capabilities, or substitution fails to resolve. - Plugin builds are included via `includeBuild` in `pluginManagement` / settings, with their own substitution semantics. - Circular inclusion between builds is rejected.

  • Does the version in `implementation("com.acme:lib:1.2")` matter when an included build provides that module?
    No — coordinate matching for includeBuild substitution is on group:name; the requested version is effectively ignored and the included project is used instead.
  • What advantage does a composite build have over `publishToMavenLocal`?
    It eliminates the publish step: the consumer builds directly against the included build's live source with incremental compilation and cross-build IDE navigation, and no stale local-repo artifacts.
  • When do you need an explicit `dependencySubstitution {}` inside includeBuild?
    When the included project's publish coordinates don't match what the consumer requests, so automatic matching can't connect them.

saying these in an interview costs you the question

  • Saying includeBuild requires manual substitution rules in the normal case — matching is automatic.
  • Believing the requested version must match the included build's version for substitution to trigger.
  • Confusing includeBuild (composite) with includeBuild in pluginManagement for plugin development without noting both exist.

context