skip to content

A multi-module build passes when run serially but intermittently fails or produces inconsistent outputs under --parallel. How do you diagnose and fix it?

level: seniorimportance: should knowfreq 30%

answer

  1. parallel reveals latent ordering bug, not a new one
  2. coupled project state OR undeclared I/O OR shared output dir
  3. build scan / --profile to see overlapping tasks
  4. grep project(":, getByName, subprojects mutation
  5. fix by deps/providers + precise @Input/@Output

basics

~20 s

Intermittent parallel-only failures usually mean coupled projects or undeclared task inputs/outputs causing races. Find the cross-project access or shared output, then fix it by declaring proper dependencies, declaring inputs/outputs accurately, or avoiding shared output paths.

solid answer

~50 s

Failures that appear only under `--parallel` are almost always **ordering assumptions that serial execution accidentally satisfied**. The usual roots: (1) **coupled projects** — build logic reading another project's mutable state (`project(":x").tasks…`, extra properties) that may now be mid-execution or, with on-demand, unconfigured; (2) **undeclared inputs/outputs**, so Gradle can't infer the implicit dependency and lets a consumer run before its producer; (3) **two tasks writing the same output directory**, racing each other. Diagnose by reproducing with a fixed seed (`--parallel`, repeat), capturing a **build scan** or `--profile` to see the actual task timeline, and grepping build logic for cross-project access. Fixes: replace cross-project reads with declared `project()` dependencies or lazy `Provider` wiring; precisely annotate `@InputFiles`/`@OutputDirectory` so Gradle re-establishes ordering; give each task a distinct output location; and ultimately adopt **Project Isolation** to make coupling a hard error rather than a latent race.

code

kotlin · 8 lines
kotlin
// BEFORE: coupled + undeclared dependency
val generated = project(":codegen").tasks.getByName("gen").outputs.files

// AFTER: declared, decoupled, ordered
val genTask = project(":codegen").tasks.named("gen")
tasks.named<JavaCompile>("compileJava") {
    inputs.files(genTask.map { it.outputs.files })
}

go deeper

for a junior

Recognize that parallel-only flakiness points to a missing dependency or race, not random behavior.

for a middle

Identify coupling and undeclared I/O as the likely causes and the corresponding fixes.

for a senior

Run a disciplined repro + build-scan diagnosis and fix via declared deps/providers and precise annotations.

for a principal

Mandate Project Isolation and CI checks so coupling/undeclared-I/O is caught early across all teams.

## Why it only breaks in parallel Serial execution imposes an *implicit total order* (project by project, task by task). Many builds silently rely on that order without declaring it. Turn on `--parallel` and tasks reorder/overlap, exposing the missing constraint as a flaky failure or wrong output. The bug was always there; parallelism just reveals it. ## The three usual culprits 1. **Coupled projects.** Logic like `project(":lib").tasks.getByName("jar").archiveFile.get()` reads another project's state. Under `--parallel` that project may be executing; under configuration-on-demand it may be unconfigured. Result: stale/missing values, races. 2. **Undeclared inputs/outputs.** If task B reads a file produced by A but A doesn't declare it as `@OutputFile` (or B doesn't declare it as `@InputFiles`), Gradle never infers `B dependsOn A`. Serially A often ran first by luck; in parallel B can start first. 3. **Shared output location.** Two tasks writing the same directory corrupt each other when they overlap. Gradle serializes tasks with *declared* overlapping outputs, but if outputs are undeclared it can't protect you. ## Diagnosis workflow ```bash # 1. Reproduce reliably for i in $(seq 1 10); do ./gradlew clean build --parallel || break; done # 2. Capture the timeline ./gradlew build --parallel --scan # or --profile for an HTML report # 3. Narrow the surface ./gradlew build --parallel --max-workers=2 ``` - A **build scan** shows which tasks ran concurrently and which produced the bad artifact — match the failure to overlapping tasks. - Grep build scripts for `project(":`, `getByName`, `subprojects {`, `allprojects {` mutating tasks, and `ext`/extra-property cross reads. - Check custom tasks for fields read/written without `@Input`/`@Output` annotations. ## Fixes - **Decouple:** convert cross-project reads into `dependencies { implementation(project(":lib")) }` or lazy `Provider` wiring (`tasks.named<Jar>("jar").flatMap { it.archiveFile }`). - **Declare I/O precisely:** add `@OutputDirectory`/`@InputFiles` so Gradle infers ordering and serializes output conflicts. - **Separate outputs:** give each task its own `layout.buildDirectory.dir("…")` so nothing shares a path. - **Use ordering rules** (`mustRunAfter`) only when there's a genuine order without a data dependency. - **Adopt Project Isolation** to turn coupling into a build-time error, preventing regressions. ## What NOT to do Don't 'fix' it by turning `--parallel` off — that hides a real correctness bug (the same race can bite the remote cache or CI sharding). Fix the underlying ordering/coupling.

  • Why is disabling --parallel the wrong fix?
    It only masks a real ordering/race bug. The same defect can surface with the remote build cache, CI sharding, or future on-demand configuration — fix the declared dependencies instead.
  • How do precise input/output annotations prevent these races?
    When B's input is A's declared output, Gradle infers B dependsOn A and serializes them; it also serializes tasks with overlapping declared outputs.
  • Which tool best shows what ran concurrently?
    A build scan (or the --profile HTML report) gives a task timeline so you can match the bad artifact to overlapping tasks.

saying these in an interview costs you the question

  • 'Just turn off --parallel' as the fix — it hides a correctness bug.
  • Blaming Gradle for nondeterminism instead of finding the undeclared dependency.
  • Adding sleeps or retries instead of declaring the real ordering.

context