With --parallel enabled, how do task and project dependencies still serialize work, and how does Gradle decide what may run at the same time?
answer
- single execution DAG across all projects
- task runs when all deps finished
- ready frontier dispatched onto worker leases
- critical path bounds speedup
- same output dir / project lock serializes
basics
~20 sGradle builds one merged task graph (a DAG). A task only runs once all its dependencies finish, so dependency edges force ordering even in parallel mode. Independent branches with no edges between them run concurrently up to the worker-lease limit.
solid answer
~40 sParallelism doesn't ignore dependencies — it respects them. Gradle computes a single **directed acyclic graph** of all requested tasks across all projects (the *execution graph*), including edges from `dependsOn`, declared inputs/outputs (implicit dependencies), and project dependencies (`:app` needs `:lib:jar`). With `--parallel`, the scheduler repeatedly looks for tasks whose dependencies are all finished and dispatches them onto free **worker leases** (`org.gradle.workers.max`). So a dependency chain `A → B → C` still runs strictly in order, while two independent chains run side by side. Additional constraints further serialize work: tasks sharing the same **output directory** or holding the same project lock can't overlap, and ordering rules like `mustRunAfter`/`shouldRunAfter` add edges without creating dependencies. The result is maximum safe concurrency bounded by the graph's critical path and the lease count.
code
kotlin · 3 linestasks.register("compileFoo")
tasks.register("compileBar") // independent -> can run in parallel
tasks.register("link") { dependsOn("compileFoo", "compileBar") } // waits for bothgo deeper
Know that dependencies still force order and only independent tasks run together.
Explain the merged DAG, the ready-frontier scheduling onto leases, and implicit input/output edges.
Reason about critical path vs core count and the extra serialization from shared outputs/locks.
Shape the module graph (flatten/widen, split god-modules) to maximize the parallel frontier across the codebase.
## One graph to rule the build After configuration, Gradle has a set of requested tasks. It expands them into a single **execution graph** — a directed acyclic graph (DAG) — by following every ordering constraint: - **Explicit:** `dependsOn` / `finalizedBy`. - **Implicit:** if task B consumes a file that task A declares as an output, Gradle infers `B dependsOn A`. This is why correctly declared `@OutputFile`/`@InputFiles` matter for both correctness and parallelism. - **Project dependencies:** `implementation(project(":lib"))` makes `:app:compileJava` depend on `:lib:jar`. - **Ordering-only:** `mustRunAfter` / `shouldRunAfter` add ordering edges that don't pull a task into the graph. ## How the scheduler runs it in parallel The daemon holds a pool of **worker leases**, sized by `org.gradle.workers.max` (default = CPU cores). The scheduler: 1. Finds *ready* tasks — every dependency completed. 2. Acquires a free lease for each and starts it on a worker thread. 3. As tasks finish and release leases, newly-ready tasks are dispatched. So concurrency = (number of independent ready tasks) capped by (free leases). A linear chain pins throughput to its **critical path** no matter how many cores you have. ## Extra serialization beyond dependency edges Even independent tasks may not overlap if they conflict: - **Same output location:** two tasks writing the same directory/file are serialized to avoid corruption. - **Project lock:** mutating a project's state requires a lock; mutating tasks serialize on it. - **Destroyables / `localState`:** declared destroy relationships force ordering so a clean doesn't race a producer. ## Practical implications ```kotlin tasks.register("b") { dependsOn("a") } // A then B, always tasks.register("report") { mustRunAfter("test") } // ordering only, no dependency ``` - A *flat, wide* module graph parallelizes well; a *deep, narrow* one is throttled by its critical path. - Missing input/output declarations can both break correctness and accidentally let conflicting tasks run together — declare them precisely. - More cores help only up to the width of the ready frontier; past that, `workers.max` and the critical path dominate.
- Why might adding more CPU cores not speed up a build?If the graph is a deep dependency chain, throughput is bounded by the critical path; extra cores have no independent ready tasks to run.
- How can two tasks with no declared dependsOn still be serialized?If they write the same output directory, share a project lock, or have destroy/local-state relationships, Gradle serializes them to avoid races.
- How does Gradle infer an implicit task dependency?When one task's declared input is another task's declared output, Gradle adds an edge automatically — making accurate @Input/@Output annotations essential.
saying these in an interview costs you the question
- Claiming --parallel ignores dependency order — it strictly respects the DAG.
- Assuming N cores gives N× speedup regardless of graph shape.
- Thinking undeclared inputs/outputs are harmless — they break implicit ordering and can cause races.