skip to content

What does it mean for projects to be 'decoupled', and why is decoupling a prerequisite for safe parallel (and configuration-on-demand) execution?

level: middleimportance: must knowfreq 55%

answer

  1. no direct access to another project's mutable state
  2. interact via project() deps + lazy providers
  3. coupling = project(":x").tasks.getByName
  4. on-demand: other project may be unconfigured
  5. parallel: data race

basics

~20 s

Decoupled projects don't directly touch each other's mutable state at configuration or execution time. They interact only through declared dependencies. Coupling (e.g. project(":other").tasks.getByName(...)) breaks parallel/on-demand assumptions because another project may not be configured or done yet.

solid answer

~40 s

Two projects are **decoupled** when neither directly accesses the other's *mutable project state* — its tasks, configurations, extensions, or extra properties — during configuration or execution. They communicate only through well-defined channels: project dependencies (`implementation(project(":lib"))`), and lazy `Provider`/`Property` wiring resolved at the right time. Coupling appears when build logic does things like `project(":other").tasks.getByName("jar").archiveFile.get()` at configuration time, or mutates another project from `allprojects {}`/`subprojects {}` cross-cutting blocks. Why it matters: with `--parallel`, the other project's tasks may be running on another thread; with configuration-on-demand the other project may **not be configured at all** yet. Reaching into it then yields nondeterministic or wrong results. Gradle treats decoupling as the contract that makes both features safe, and Project Isolation enforces it strictly.

code

kotlin · 6 lines
kotlin
// Decoupled: consume another project's output lazily
val libJar = project(":lib").let { p ->
    p.tasks.named<Jar>("jar").flatMap { it.archiveFile }
}
// Prefer expressing this as a real dependency instead:
dependencies { implementation(project(":lib")) }

go deeper

for a junior

Recognize the term and that coupled builds aren't safe to parallelize.

for a middle

Give concrete coupling anti-patterns and the decoupled alternatives (project deps, providers).

for a senior

Explain why on-demand + parallel both rely on decoupling and how races/nondeterminism arise.

for a principal

Drive a migration to convention plugins + Provider API and adopt Project Isolation to enforce the contract repo-wide.

## The mental model Think of each project as a service with a public API. Its **internal mutable state** — `tasks`, `configurations`, `extensions`, `project.ext` / extra properties, the `Project` object itself — is private. Two projects are **decoupled** when neither pokes at the other's private state at configuration or execution time. ## Allowed (decoupled) interactions - **Project dependencies:** `implementation(project(":core"))` — Gradle wires the artifact/classpath edges and orders work correctly. - **Lazy providers:** publishing an output via `Provider`/`Property` (e.g. a task output `RegularFileProperty`) and consuming it through `flatMap`, so the value is computed only when needed. - **Shared conventions applied independently** (a convention plugin each project applies), rather than one project mutating another. ## Coupling anti-patterns ```kotlin // COUPLED — bad: reaches into another project's task at configuration time val otherJar = project(":other").tasks.getByName("jar") // COUPLED — bad: cross-project mutation subprojects { // mutating each subproject's tasks/extensions from the root } ``` These assume the other project is already configured and its tasks are in a final state — assumptions that break under parallelism and on-demand configuration. ## Why decoupling is the prerequisite - **`--parallel`:** the other project's tasks may be executing concurrently. Reading its mutable state from your thread is a data race. - **Configuration on demand:** Gradle skips configuring projects it believes aren't needed. A coupled reference may hit an *unconfigured* project, so the state you read is missing or wrong. - **Determinism:** coupled builds can produce different results depending on scheduling/order, which is exactly what reproducible builds must avoid. ## How Gradle helps you stay decoupled - **Provider API** lets you wire outputs to inputs lazily without grabbing the other project early. - **Project Isolation** (an evolving feature) actively forbids cross-project access and turns coupling into hard errors, making `--parallel` and configuration-on-demand reliably safe. The practical rule: never call `project(":x").<mutable state>` from build logic. Express the relationship as a dependency or a provider instead.

  • Name a concrete coupling anti-pattern.
    Calling project(":other").tasks.getByName("jar") at configuration time, or mutating subprojects' tasks/extensions from a root allprojects/subprojects block.
  • Why does configuration-on-demand make coupling especially dangerous?
    On-demand may skip configuring the referenced project entirely, so your cross-project read hits state that was never set up.
  • What feature enforces decoupling?
    Project Isolation turns cross-project state access into errors, guaranteeing the build is safe for parallel and on-demand execution.

saying these in an interview costs you the question

  • Saying decoupling just means 'no shared code' — it's about not touching another project's mutable state.
  • Believing project dependencies cause coupling — they're the sanctioned, decoupled channel.

context