skip to content

What constraints and gotchas should you keep in mind when structuring composite builds with includeBuild?

level: seniorimportance: nice to knowfreq 22%

answer

  1. no cycles, no self-inclusion
  2. unique group:name across the composite
  3. root build is the entry point / declares composition
  4. independent config = extra cost
  5. silent fallback on coordinate typos

basics

~20 s

Each included build is standalone with its own settings; composition can nest but you can't create cycles where builds include each other. Included builds configure independently, adding overhead, and only the root build's settings declares the composition that the CLI runs against.

solid answer

~50 s

Composite builds compose along a directed structure: a build includes others, which may themselves include further builds. The hard rule is **no cycles** — two builds cannot mutually `includeBuild` each other, and you cannot include yourself. Practically, watch a few things: (1) each included build **configures independently**, so a large composite has more configuration cost than the equivalent single multi-project build; (2) the **outer (root) build** is the one you invoke and whose settings declares the composition — included builds don't 'see' the outer build; (3) coordinate-based substitution is **silent** when it doesn't match, so structural mistakes fail quietly by falling back to published artifacts; (4) build identity (group/name) must be unique across the composite to avoid ambiguous substitution. Keep the graph shallow and the coordinates clean. Reserve composites for genuinely independent builds; deeply nested composites are hard to reason about.

code

kotlin · 6 lines
kotlin
// app/settings.gradle.kts -- the root build you actually run
rootProject.name = "app"
includeBuild("../platform")
includeBuild("../my-library")
// VALID: app -> platform, app -> my-library
// INVALID: my-library/settings.gradle.kts must NOT includeBuild("../app") -> cycle

go deeper

for a junior

Know the included build is standalone and you can't make builds include each other.

for a middle

Explain that composition is acyclic and the root build is the entry point declaring it.

for a senior

Anticipate independent-configuration cost, silent substitution failures, and unique-coordinate requirements.

for a principal

Govern composite topology org-wide: shallow graphs, unique coordinates, composites only for independently-released builds, multi-project otherwise.

## The shape of a composite A composite is a **directed acyclic** arrangement of builds: the build you run is the root, and `includeBuild` adds children, which may include grandchildren. Gradle flattens these for execution, but conceptually the root drives everything. ### Hard constraints - **No cycles.** Build A cannot `includeBuild` B while B `includeBuild`s A. Self-inclusion is also rejected. - **Unique identity.** Each included build's `group:name` coordinates should be unique within the composite; ambiguous coordinates make substitution undefined. - **The root build is the entry point.** You invoke tasks from the outer build; included builds are nested participants and do not treat the outer build as their parent. ### Performance and ergonomics gotchas 1. **Independent configuration cost.** Every included build runs its own configuration phase. A composite of many builds configures more slowly than the same code as subprojects of one build. Don't reach for composites just to 'organize' code that ships together. 2. **Silent substitution failures.** Because matching keys on `group:name`, a typo in `group` doesn't error — Gradle just resolves the published artifact and your local edits seem ignored. Verify coordinates first when 'it didn't pick up my change.' 3. **Settings live in the consumer.** The composition is declared in the **root** build's `settings.gradle.kts` (or via `--include-build`). Editing the included build's settings doesn't compose anything into the outer build. 4. **Mental overhead.** Many standalone builds, each with its own settings and lifecycle, are harder to reason about than one multi-project build. Keep the nesting shallow. ## A sensible structure ```kotlin // app/settings.gradle.kts (the build you run) rootProject.name = "app" includeBuild("../platform") // standalone build includeBuild("../my-library") // standalone build ``` Here `app` is the root entry point; `platform` and `my-library` are independent builds composed in. Neither included build references `app`. ## Rule of thumb Use a composite to keep independent, separately-released builds developable together. Keep the graph shallow, the coordinates unique and correct, and prefer a single multi-project build whenever the pieces actually ship as one unit.

  • Why is a deep composite slower than the equivalent multi-project build?
    Each included build configures independently, so a composite repeats configuration work that a single build's shared configuration phase would do once.
  • Can two builds include each other?
    No — that forms a cycle, which Gradle rejects. Composition must be acyclic.

Think of it like importing libraries by dependency graph: imports can chain but a circular import is forbidden, and a misspelled name silently resolves to the wrong package rather than erroring.

saying these in an interview costs you the question

  • Believing builds can mutually includeBuild each other — cycles are forbidden.
  • Treating composites as a free organizational tool — they add configuration overhead versus subprojects.
  • Assuming a coordinate typo will produce an error rather than a silent fallback to the published artifact.

context