skip to content

Why is applying convention plugins per subproject considered the isolation-preserving alternative to subprojects {}? What concretely does it preserve?

level: seniorimportance: must knowfreq 50%

answer

  1. no cross-project access at config time
  2. plugin runs against own Project
  3. Configuration Cache / Project Isolation
  4. parallel + lazy configuration
  5. direction of control flips

basics

~20 s

Applying a plugin configures only the module that applies it, using only inputs the plugin itself knows. subprojects {} reaches across project boundaries at configuration time, coupling projects and breaking the isolation Gradle wants for safe parallel/lazy configuration.

solid answer

~40 s

Project isolation means each project is configured without reading or mutating the state of other projects. `subprojects { }` violates this: the root iterates over and configures every child, so the root's configuration depends on and writes into children. That cross-project access is exactly what Gradle's Configuration Cache and the experimental Project Isolation feature try to eliminate, because it prevents reliable caching and parallel configuration. Applying a convention plugin per subproject preserves isolation because the plugin runs *within* the applying project and configures only that project, from inputs the plugin packages itself (toolchain version, dependency coordinates). No project reaches into another. Each module's configuration is self-contained, which is what lets Gradle cache and parallelize configuration safely. The shared logic is reused without shared mutable cross-project access.

code

kotlin · 9 lines
kotlin
// Isolation-breaking (root reaches into children)
subprojects {
    apply(plugin = "java")
    repositories { mavenCentral() }
}

// Isolation-preserving (child applies a packaged preset)
// child/build.gradle.kts
plugins { id("myproject.java-conventions") }

go deeper

for a junior

Know the slogan: applying a plugin only configures the module that applies it, unlike subprojects {}.

for a middle

Define cross-project access at configuration time and why subprojects {} causes it.

for a senior

Connect to Configuration Cache, Project Isolation, and parallel configuration; note convention plugins can still break isolation if written badly.

for a principal

Discuss adopting Project Isolation as a build-platform goal and the migration cost of eliminating subprojects {} across a large monorepo.

## What "project isolation" means Gradle configures a build in two big phases: **configuration** (evaluate every project's build script, register tasks, wire config) and **execution** (run the selected tasks). *Project isolation* is the principle that during configuration, each project is evaluated **without reading or mutating another project's model**. When that holds, Gradle can: - cache the configured model (Configuration Cache), - configure projects in parallel, - and safely skip re-configuring unchanged projects. ## How subprojects {} breaks it ```kotlin // root build.gradle.kts — the anti-pattern here subprojects { apply(plugin = "java") java { toolchain { languageVersion.set(JavaLanguageVersion.of(21)) } } dependencies { /* ... */ } } ``` This block lives in the root and, at configuration time, **reaches into every child project** to mutate it. The root's configuration now depends on the existence and state of all children, and writes into them. That is cross-project access. The Configuration Cache and the experimental **Project Isolation** feature flag both flag this pattern, because a cached/isolated model cannot tolerate one project arbitrarily mutating another. ## How applying convention plugins preserves isolation ```kotlin // each module configures itself plugins { id("myproject.java-conventions") } ``` When the module is configured, the convention plugin's `apply` logic runs **against that module's own `Project`**. It only touches the project applying it, and it pulls its inputs from things the plugin packages (a hardcoded toolchain version, a version-catalog reference, dependency coordinates). No `Project` reaches into a sibling or the root. Each module's configuration is therefore self-contained and deterministic from its own declarations. ## Why this is the *concrete* preserved property What is preserved is **the absence of cross-project state access during configuration**. That is the precondition for: - **Configuration Cache hits** — the cached entry for a project stays valid because its config didn't depend on mutating others. - **Parallel configuration** — projects can be configured independently. - **Local reasoning** — to know a module's config you read its script + the applied plugins, never the whole tree. ## A nuance worth stating Convention plugins are isolation-*friendly*, but you can still break isolation inside one (e.g. if the plugin does `project.rootProject.subprojects { ... }` or reads another project's tasks). The benefit comes from applying them per project and keeping their logic project-local — not merely from the fact that they are plugins. ## Summary Same shared logic, but the direction of access flips: instead of the root reaching *out* to children, each child reaches *in* to a packaged preset. That flip is what keeps configuration isolated, cacheable, and parallelizable.

  • Does merely using a convention plugin guarantee isolation?
    No. If the plugin's own logic reaches into other projects (rootProject.subprojects { }, reading sibling tasks), it reintroduces cross-project access. Isolation comes from keeping the plugin's effects project-local.
  • Which Gradle features specifically benefit from this isolation?
    The Configuration Cache (cached configured model) and the experimental Project Isolation feature, plus parallel/lazy configuration — all require that configuring one project doesn't read or mutate another.
  • Is subprojects {} forbidden then?
    Not forbidden, but discouraged for sharing config; it works yet undermines caching and isolation. Convention plugins are the recommended replacement for that use.

saying these in an interview costs you the question

  • Saying convention plugins are faster simply because they are 'compiled' — the real win is isolation enabling caching/parallelism.
  • Assuming any convention plugin is automatically isolation-safe regardless of what its body does.
  • Confusing this with runtime classpath isolation; it's about configuration-time cross-project access.

context