What does it mean for projects to be 'decoupled', and why is decoupling a prerequisite for safe parallel (and configuration-on-demand) execution?
answer
- no direct access to another project's mutable state
- interact via project() deps + lazy providers
- coupling = project(":x").tasks.getByName
- on-demand: other project may be unconfigured
- parallel: data race
basics
~20 sDecoupled 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 sTwo 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// 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
Recognize the term and that coupled builds aren't safe to parallelize.
Give concrete coupling anti-patterns and the decoupled alternatives (project deps, providers).
Explain why on-demand + parallel both rely on decoupling and how races/nondeterminism arise.
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.