skip to content

How does finalizedBy differ from dependsOn and from mustRunAfter/shouldRunAfter?

level: middleimportance: must knowfreq 50%

answer

  1. two axes: pulls-in? + on-failure?
  2. dependsOn = before, pulls in
  3. mustRun/shouldRunAfter = order only, pulls nothing
  4. finalizedBy = after + on failure + pulls in
  5. cleanup => finalizedBy, never mustRunAfter

basics

~10 s

dependsOn pulls a task in to run before; mustRunAfter/shouldRunAfter only constrain ordering when both tasks are already in the graph. finalizedBy schedules a task to run after — even on failure — for cleanup.

solid answer

~50 s

These four relationships answer different questions. - **`dependsOn`**: 'this task needs X first.' It *pulls X into the graph* and runs it before. If X fails, the dependent never runs. - **`mustRunAfter` / `shouldRunAfter`**: pure *ordering* constraints. They do **not** pull tasks in; they only say 'if both are in the graph, order them this way.' `mustRunAfter` is hard, `shouldRunAfter` is a soft preference Gradle can drop to avoid cycles or improve parallelism. - **`finalizedBy`**: schedules a finalizer to run *after* the finalized task, **including on failure**. It pulls the finalizer in *because* the finalized task is scheduled. The distinguishing axes are (1) does it pull the other task into the graph, and (2) does it run on failure. `dependsOn` pulls in (before). `finalizedBy` pulls in (after, even on failure). Ordering rules pull nothing in. Only `finalizedBy` gives teardown-on-failure semantics, which is why it's the right tool for cleanup.

code

kotlin · 5 lines
kotlin
tasks.named("integrationTest") {
    dependsOn("startServer")    // prerequisite, before, fails-stop
    finalizedBy("stopServer")   // teardown, after, even on failure
    mustRunAfter("test")        // ordering only, pulls nothing in
}

go deeper

for a junior

Know finalizedBy = cleanup-after; dependsOn = prerequisite-before. Don't worry about the ordering pair yet.

for a middle

Articulate the two axes (pulls-in?, on-failure?) and correctly slot all four relationships.

for a senior

Explain combining dependsOn + finalizedBy for setup/teardown and why ordering rules can't substitute.

for a principal

Discuss how choosing the right relationship affects graph determinism, parallelism, and reusable convention-plugin design.

## The four task relationships Gradle lets you wire tasks together with four distinct relationships. Confusing them is a classic interview trap, so let's pin down exactly what each does along two axes: **does it add the other task to the execution graph**, and **what is the ordering/failure behavior**. ### 1. `dependsOn` — prerequisite, runs before ```kotlin tasks.named("test") { dependsOn("compileJava") } ``` - **Pulls in**: yes. Requesting `test` pulls `compileJava` into the graph. - **Order**: dependency runs *before* the dependent. - **On failure**: if `compileJava` fails, `test` does not run. ### 2. `mustRunAfter` — hard ordering, pulls nothing in ```kotlin tasks.named("integrationTest") { mustRunAfter("test") } ``` - **Pulls in**: no. If only `integrationTest` is requested, `test` does not run. - **Order**: *if* both are in the graph, `integrationTest` runs after `test`. - **On failure**: irrelevant — it's just ordering. ### 3. `shouldRunAfter` — soft ordering Same as `mustRunAfter` but it's advisory: Gradle will honor it when possible but **drops** it to break a cycle or when it would otherwise block parallel/optimal scheduling. ### 4. `finalizedBy` — teardown, runs after even on failure ```kotlin tasks.named("integrationTest") { finalizedBy("stopServer") } ``` - **Pulls in**: yes — the finalizer is scheduled because its finalized task is scheduled. - **Order**: finalizer runs *after* the finalized task. - **On failure**: finalizer **still runs** (as long as the finalized task actually began executing). ## Decision table | Need | Use | |------|-----| | X must complete successfully before Y | `Y.dependsOn(X)` | | Y after X only when both run, hard | `Y.mustRunAfter(X)` | | Y after X when convenient, soft | `Y.shouldRunAfter(X)` | | Cleanup C after Y, even if Y fails | `Y.finalizedBy(C)` | ## Combining them Real teardown wiring usually combines two: ```kotlin tasks.named("integrationTest") { dependsOn("startServer") // bring server up first finalizedBy("stopServer") // tear it down after, win or lose } ``` `dependsOn` guarantees setup, `finalizedBy` guarantees teardown. Neither `mustRunAfter` nor `shouldRunAfter` would help here, because they neither pull `stopServer` in nor run it on failure. ## Why this matters Picking the wrong relationship produces silent bugs: using `mustRunAfter` for cleanup means the cleanup simply never runs unless someone also requested it; using `dependsOn` for cleanup means it runs *before*, not after, and not on failure. `finalizedBy` is the only one with true after-even-on-failure semantics.

  • Could you replace finalizedBy with mustRunAfter for cleanup?
    No. mustRunAfter only orders tasks that are both already scheduled and doesn't run on failure, so cleanup would often not run at all. finalizedBy pulls the cleanup in and runs it even when the main task fails.
  • If integrationTest is finalizedBy stopServer, and you run only stopServer, does integrationTest run?
    No. Finalization is one-directional: requesting the finalizer does not pull in the finalized task. stopServer just runs by itself.
  • What's the difference between mustRunAfter and shouldRunAfter?
    Both are ordering-only and pull nothing in. mustRunAfter is a hard constraint; shouldRunAfter is advisory and Gradle drops it to break cycles or improve scheduling.

saying these in an interview costs you the question

  • Saying mustRunAfter pulls the other task into the graph — it does not.
  • Treating finalizedBy and dependsOn as interchangeable — direction and failure behavior differ.
  • Claiming shouldRunAfter is always honored — it can be silently dropped.

context