skip to content

Gradle fails graph construction with a circular dependency error. What does this mean, and how do you diagnose and resolve it?

level: seniorimportance: should knowfreq 40%

answer

  1. DAG can't have cycles -> fails at construction
  2. read the printed cycle path
  3. dependsOn misused for ordering
  4. demote to shouldRunAfter / extract shared task
  5. shouldRunAfter cycle is dropped, not fatal

basics

~20 s

It means the dependency edges form a cycle, so no valid execution order exists. Read the printed cycle path, then break it — usually by replacing a wrong dependsOn with an ordering hint or by extracting shared work into a third task.

solid answer

~40 s

Because the graph must be a DAG, a cycle (A → B → … → A through `dependsOn`/inferred edges) makes a topological order impossible, so Gradle aborts during graph construction *before* execution. The error message prints the **exact cycle path** — read it carefully; the offending edge is usually a `dependsOn` that should have been a `mustRunAfter`/`shouldRunAfter` (ordering, not membership) or a provider mis-wired so a producer accidentally consumes its consumer's output. Resolution patterns: (1) demote a hard `dependsOn` to `shouldRunAfter` when you only need ordering; (2) **extract** the shared logic both tasks need into a third task both depend on, breaking the loop; (3) fix accidental output→input wiring that points the wrong way. `shouldRunAfter` cycles are special — Gradle silently drops the soft constraint rather than failing.

code

kotlin · 4 lines
kotlin
// Cycle: taskA <-> taskB via dependsOn
// Fix: keep one hard edge, use an ordering hint for the other
taskB { dependsOn(taskA) }
taskA { shouldRunAfter(taskB) }  // relaxed automatically if it would cycle

go deeper

for a junior

Recognize the error means dependency edges form a loop.

for a middle

Read the cycle path and demote a misused dependsOn to an ordering hint.

for a senior

Apply extract-shared-task and provider-direction fixes; distinguish soft vs hard cycles.

for a principal

Establish conventions (provider wiring, ordering hints over dependsOn) so multi-module builds avoid cyclic lifecycle tangles by design.

## What the error means The task graph is a **DAG**. A **circular dependency** means following dependency edges leads back to the starting task. There's no order where every task precedes its dependents, so Gradle fails at **graph-construction time**, before any action runs: ``` Circular dependency between the following tasks: :taskA \--- :taskB \--- :taskA (*) ``` The printed tree **is** your diagnostic — it names every task in the loop. ## How cycles sneak in 1. **Wrong relationship type** — using `dependsOn` for pure ordering. If `clean.dependsOn(jar)` and `jar.dependsOn(clean)` you've made a loop; one side only needed *ordering*. 2. **Mis-wired providers** — a producer task's input is accidentally set from a task that consumes the producer's output, reversing an edge. 3. **Aggregate-task tangles** — lifecycle tasks depending on each other in a multi-module setup. ## Resolution patterns ### Demote to an ordering hint If you only need 'run X after Y *when both run*', use `mustRunAfter`/`shouldRunAfter` instead of `dependsOn` — ordering hints don't form hard cycles, and `shouldRunAfter` is even dropped automatically to break a soft cycle. ```kotlin // Before: cycle taskB.dependsOn(taskA) taskA.dependsOn(taskB) // After: keep one hard edge, make the other an ordering hint taskB.dependsOn(taskA) taskA.shouldRunAfter(taskB) // dropped if it would create a cycle ``` ### Extract shared work If A and B both `dependsOn` each other because each needs something the other produces, pull that shared step into a new task C, and make both A and B depend on C. The cycle becomes a diamond. ### Fix the wiring direction With provider-based inputs, confirm the **consumer** sets its input from the **producer**, not vice-versa. ## Soft vs hard cycles A cycle formed only by `shouldRunAfter` is **not** an error — Gradle relaxes the soft constraint. Only `dependsOn`/`finalizedBy`/inferred edges (and `mustRunAfter` if it strictly conflicts) cause the fatal cycle. ## Tooling `--dry-run` won't help once construction fails, but reducing scope (request fewer tasks) narrows which edges introduce the loop; the printed cycle path is the primary tool.

  • Why doesn't a shouldRunAfter cycle fail the build?
    shouldRunAfter is a soft ordering preference. When honoring it would create a cycle, Gradle simply drops the constraint and proceeds, so it never produces a fatal circular dependency.
  • Two tasks each depend on the other because each needs the other's product. How do you break it cleanly?
    Extract the shared work into a third task both depend on, turning the cycle into a diamond. Often the real issue is that one 'dependency' was only an ordering need that should be mustRunAfter/shouldRunAfter.

saying these in an interview costs you the question

  • Trying to 'force' execution order with more dependsOn edges, which deepens the cycle.
  • Assuming the cycle is a Gradle bug rather than reading the printed cycle path.
  • Confusing a soft shouldRunAfter constraint with a hard cycle.

context