skip to content

How does cross-configuration with subprojects { } / allprojects { } interfere with Gradle's configuration cache?

level: seniorimportance: must knowfreq 48%

answer

  1. per-project config = function of own inputs
  2. root becomes an input to every child
  3. blocks parallel + independent caching
  4. coarse invalidation
  5. convention plugins restore the invariant

basics

~10 s

The configuration cache assumes each project's configuration depends only on its own inputs. Cross-configuration makes the root produce other projects' config, breaking that per-project assumption and limiting safe reuse and parallel configuration.

solid answer

~50 s

Gradle's **configuration cache** stores the result of the configuration phase so subsequent builds can skip it. Its design assumes the configuration of each project is a function of **that project's own inputs**, which lets Gradle (with Project Isolation) configure and cache projects independently and in parallel. `subprojects { }`/`allprojects { }` violate this: the root project's script mutates other projects, so a child's configured state is actually produced by code running in the root. That entangles cache entries — the root effectively becomes an input to every child — and prevents Gradle from isolating, parallelizing, or independently invalidating per-project configuration. It also tends to drag broad, eagerly-evaluated state into the cached model. Convention plugins keep each project's configuration self-contained, so the cache can key each project on its own inputs and reuse them safely. Note the configuration cache still *functions* with cross-config in many cases, but you forfeit the isolation/parallelism benefits and risk harder-to-cache patterns.

code

kotlin · 9 lines
kotlin
// Cross-config: child's Test config is produced by the ROOT.
// Root becomes an input to every child -> can't isolate/parallelize cache.
subprojects {
    tasks.withType<Test>().configureEach { useJUnitPlatform() }
}

// Convention-plugin equivalent: child configures itself, cache keys on its own inputs.
// build-logic/.../myorg.test-conventions.gradle.kts
tasks.withType<Test>().configureEach { useJUnitPlatform() }

go deeper

for a junior

Recall that the config cache prefers each project to depend only on its own inputs, and cross-config breaks that.

for a middle

Explain that the root becomes an input to children and why that blocks independent caching/invalidation.

for a senior

Articulate the parallel-configuration / Project Isolation link and the nuance that the cache may still run but loses isolation benefits.

for a principal

Quantify the build-performance argument for org-wide migration and govern config-cache compatibility as a CI gate.

## What the configuration cache is The **configuration cache** is a Gradle feature that serializes the result of the *configuration phase* (the task graph and configured task state) to disk. On the next compatible invocation, Gradle skips configuration entirely and goes straight to execution, which can dramatically speed up builds — especially large multi-project ones. ## The invariant it relies on For the cache to be correct and to enable **parallel project configuration** (under the incubating **Project Isolation** feature), Gradle wants each project's configuration to be a pure function of **that project's own declared inputs** (its build script, its `gradle.properties`, value sources, etc.). If that holds, Gradle can: - configure projects **independently** (and in parallel), - **cache** each project's configuration separately, - **invalidate** only the projects whose own inputs changed. ## How cross-configuration breaks the invariant With `subprojects { }`/`allprojects { }`, the **root** build script contains code that mutates child projects: ```kotlin subprojects { tasks.withType<Test>().configureEach { useJUnitPlatform() } } ``` Now a child project's configured state is **not** purely a function of its own inputs — it is produced by the root. Effects: - The **root becomes an input** to every child's configuration, entangling cache entries and serializing what could have been independent work. - Gradle cannot safely **isolate** or **parallelize** per-project configuration, because configuring one project requires running the root's cross-config code. - Invalidation gets coarser: a change in the root's cross-config can invalidate *all* projects. - Cross-config blocks often pull broad, **eagerly-evaluated** state (project references, resolved values) into the model, which is exactly the kind of thing the cache must serialize and the lazy `Provider`/`Property` APIs try to avoid. ## Important nuance The configuration cache does **not** outright reject `subprojects { }` in every case — many builds run with both. But you lose the **isolation and parallel-configuration** benefits, and you make it easier to write captured-state patterns the cache can't serialize. The clean path is convention plugins. ## Convention plugins restore the invariant When shared logic lives in a convention plugin that each project applies to itself, each project's configuration is again self-contained: ```kotlin // subproject build.gradle.kts plugins { id("myorg.test-conventions") } ``` The plugin's code runs *within that project's* configuration, keyed on that project's inputs. The cache can store and reuse each project independently — and Project Isolation can run them in parallel.

  • Does the configuration cache flatly reject builds that use subprojects { }?
    Not necessarily — many builds use both. The cache may still work, but cross-configuration prevents the per-project isolation/parallel-configuration that Project Isolation provides and makes captured-state problems more likely.
  • What kind of code inside subprojects { } is most likely to actually fail the configuration cache?
    Capturing live objects into task actions or doLast {} (e.g. referencing project, another project, or unresolved mutable state at execution time). The cache can't serialize the live Project, so it reports a problem.
  • How does Project Isolation relate to the configuration cache here?
    Project Isolation is the incubating feature that enforces the per-project invariant the cache wants, enabling parallel configuration. Cross-configuration is one of the main patterns it flags.

saying these in an interview costs you the question

  • Saying the configuration cache 'doesn't care' about cross-configuration
  • Claiming convention plugins are only about DRY, not caching/parallelism
  • Confusing the configuration cache with the build cache (task outputs)

context