skip to content

Why Cross-Configuration Breaks Isolation

Why cross-project configuration violates project isolation and defeats the configuration cache, and why convention plugins replaced it. A pointed question about whether you have kept up with where Gradle is heading.

on this pageshow

questions

5

What is 'project isolation' in Gradle, and why does cross-configuration with subprojects { } or allprojects { } violate it?

level: middleimportance: must knowfreq 55%

answer

  1. configure only your own project
  2. root mutating children = cross-project mutation
  3. couples + eager + lock-step
  4. breaks config-cache per-project invariant
  5. convention plugin = self-apply

basics

~10 s

Project isolation means a project only reads/mutates its own state, never another project's. subprojects { } / allprojects { } reach into other projects' models from the root, coupling them and breaking that boundary.

solid answer

~50 s

Project isolation is the principle (and an incubating Gradle feature) that during configuration a project must only access and mutate **its own** state — never another project's. Cross-configuration blocks like `subprojects { }` and `allprojects { }` run code from the root build script that directly mutates child projects (their plugins, dependencies, extensions). That is *cross-project mutation*: the root reaches across the boundary into other projects. It couples all projects together, forces the whole build model to be configured eagerly and in lock-step, and makes it impossible for Gradle to evaluate or invalidate projects independently. It also defeats the configuration cache, which assumes each project's configuration is a function of its own inputs. The modern alternative is to push shared logic into **convention plugins** that each project *applies* to itself, so every project configures only itself.

code

kotlin · 9 lines
kotlin
// BAD: root build.gradle.kts mutates every child (cross-project mutation)
subprojects {
    apply(plugin = "java")
    dependencies { "implementation"("org.slf4j:slf4j-api:2.0.13") }
}

// GOOD: each subproject applies a convention plugin to ITSELF
// subproject build.gradle.kts:
plugins { id("myorg.java-conventions") }

go deeper

for a junior

Know the definition: a project should configure only itself; subprojects/allprojects break that by changing other projects from the root.

for a middle

Explain cross-project mutation, the coupling/eager-configuration cost, and that convention plugins are the fix.

for a senior

Tie it to the configuration cache's per-project invariant and the incubating Project Isolation feature; explain why self-application preserves it.

for a principal

Frame an org-wide migration strategy off cross-configuration toward convention plugins, and the build-performance/governance payoff of enforced isolation.

## What project isolation means **Project isolation** is the rule that, during the *configuration phase*, a project may only read and mutate **its own** state — its own extensions, tasks, dependencies, and properties. It must not reach into a *sibling* or *child* project and change that project's model. Gradle is turning this into an enforced, incubating feature (Project Isolation), but even before enforcement it is the design assumption behind the **configuration cache** and parallel configuration. ## Why cross-configuration violates it A root `build.gradle.kts` like this: ```kotlin subprojects { apply(plugin = "java") dependencies { "implementation"("org.slf4j:slf4j-api:2.0.13") } } ``` runs inside the **root** project's configuration but **mutates every child project** — applying plugins, adding dependencies. This is *cross-project mutation*: code owned by one project changes another project's model. Consequences: - **Coupling**: every subproject now depends on the root being configured first, in a specific order. You cannot reason about a subproject in isolation. - **Eager, lock-step configuration**: the root must visit all children to configure them, so Gradle cannot lazily skip or independently evaluate projects. - **Configuration-cache hostility**: the cache keys each project's configuration on *its own* inputs. When one project's config is actually produced by code in another project, that invariant is broken, so the cache can't safely reuse per-project results. ## The deprecation direction Gradle has been steadily deprecating and warning against *accessing the mutable state of another project*. With Project Isolation enabled, patterns that mutate other projects (including much of what `subprojects/allprojects` does) are reported as violations. The guidance is unambiguous: **stop cross-configuring; let each project configure itself.** ## The preferred alternative — convention plugins Move the shared logic into a **convention plugin** (a precompiled script plugin or a `Plugin<Project>` implementation, typically in an included `build-logic` build). Each subproject then does: ```kotlin plugins { id("myorg.java-conventions") } ``` Now the *only* code mutating a project lives in a plugin **that project applies to itself**. There is no cross-project mutation, isolation holds, and the configuration cache can treat each project independently. The DRY benefit is the same as `subprojects { }`, but without the boundary violation.

  • Does using only allprojects { } in the root for plugin application also violate isolation?
    Yes — applying a plugin to another project from the root still mutates that project's model. The violation is the cross-project mutation, regardless of which block (allprojects or subprojects) performs it.
  • Is reading another project's property as bad as mutating it?
    Both are problematic under strict Project Isolation, but mutation is the harder violation. Reading another project's mutable state still couples builds and is also flagged; the safe path is sharing data via well-defined APIs (e.g. shared build services or published outputs), not direct cross-project access.

subprojects { } is like a manager rewriting everyone's to-do list from a central desk; convention plugins hand each person the same checklist to follow themselves.

saying these in an interview costs you the question

  • Claiming subprojects { } is fine because 'it's just DRY'
  • Thinking isolation only matters at runtime/execution, not configuration
  • Believing convention plugins and subprojects { } are equivalent for isolation

context

open as a page

Why are convention plugins preferred over subprojects { }/allprojects { } for sharing build logic?

level: middleimportance: must knowfreq 50%

basics

~20 s

Convention plugins are applied by each project to itself, so they preserve project isolation, stay configuration-cache friendly, are explicit and opt-in per project, and are testable/versioned — unlike cross-config that mutates projects from the root.

open as a page

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

level: seniorimportance: must knowfreq 48%

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.

open as a page

What does Gradle's deprecation of 'mutating another project's state' mean, and which common subprojects { } patterns does it target?

level: middleimportance: should knowfreq 38%

basics

~20 s

Gradle is deprecating code that reaches into and changes another project's model from outside it — exactly what subprojects { }/allprojects { } and project(":x") { ... } blocks do. The fix is self-applied convention plugins.

open as a page

Beyond isolation, what eager-evaluation and ordering pitfalls does cross-configuration cause, and how do convention plugins avoid them?

level: seniorimportance: should knowfreq 30%

basics

~10 s

subprojects { } eagerly visits every project, often forcing afterEvaluate hooks and order-dependent code that's fragile. Convention plugins configure one project lazily with configureEach/register, removing the ordering traps.

open as a page