skip to content

Lifecycle Hooks and Listeners

The hooks that let build logic run at defined moments: project evaluation, build completion, task listeners, and shared services. Interviewers ask because several of these are legacy and the configuration cache has replaced them.

on this pageshow

explore

questions

20

How do you run code after a Gradle build finishes, and what does gradle.buildFinished give you access to?

level: juniorimportance: must knowfreq 55%

answer

  1. fires after build, success or failure
  2. BuildResult.getFailure() null on success
  3. classic teardown/cleanup hook
  4. NOT configuration-cache safe
  5. register in init script or plugin

basics

~20 s

Register a callback with gradle.buildFinished { result -> ... }. It runs once the build ends and gives you a BuildResult with result.failure, so you can do cleanup or reporting whether the build passed or failed.

solid answer

~40 s

`gradle.buildFinished` is a lifecycle hook that fires once after the build completes — on success and on failure. The closure receives a `BuildResult` exposing `getFailure()` (the `Throwable` that ended the build, or null on success) and `getAction()` (e.g. "Build"). You typically register it in an init script or plugin to run cleanup, stop daemons you started, or emit a custom report. It runs even if a task failed, which makes it the classic place for teardown. ```kotlin gradle.buildFinished { if (it.failure != null) println("Build failed: ${it.failure?.message}") } ``` Note: `buildFinished` is **not** compatible with the configuration cache. Modern Gradle steers you to the Flow API (`FlowScope`/`FlowAction`) instead for new code.

code

kotlin · 7 lines
kotlin
gradle.buildFinished { result ->
    if (result.failure != null) {
        logger.error("Build failed", result.failure)
    } else {
        logger.lifecycle("Build succeeded")
    }
}

go deeper

for a junior

Know the closure form gradle.buildFinished { ... }, that it runs at the end, and that result.failure tells you if it failed.

for a middle

Explain BuildResult contents, where you'd register it (init script vs plugin), and that it fires on the failure path for teardown.

for a senior

Flag the configuration-cache incompatibility and that the Flow API is the replacement for new code.

for a principal

Frame org-wide build telemetry/reporting via init scripts and the migration policy away from buildFinished across many repos.

## What `buildFinished` is Gradle runs in phases: **initialization**, **configuration**, then **execution**. The `Gradle` object (available as `gradle` in build scripts, settings scripts, and init scripts) exposes lifecycle callbacks. `gradle.buildFinished(closure)` registers an action that runs **once**, after the entire build has finished — regardless of whether it succeeded or failed. ## The `BuildResult` you receive The callback is handed a `org.gradle.BuildResult`: - `getFailure(): Throwable?` — the exception that terminated the build, or `null` if it succeeded. - `getAction(): String` — what Gradle was doing (typically `"Build"`). - `getGradle(): Gradle` — back-reference to the build. This is why `buildFinished` is the canonical teardown hook: it fires on the failure path too, so resources you spun up (test fixtures, external processes, temp directories, a started database) get cleaned up. ## Where to register it - **Init script** (`~/.gradle/init.gradle(.kts)` or `--init-script`) — applies to every build; good for org-wide reporting. - **Settings script** or a **plugin** — scoped to one project tree. ## The big caveat: configuration cache `buildFinished` captures a closure that may reference live build model objects (the `Project`, `Task` graph, services). Those cannot be serialized, so **`buildFinished` is incompatible with the configuration cache** and Gradle emits a problem when it is used under `--configuration-cache`. For new code Gradle recommends the **Flow API** (`FlowScope.always { ... }` registering a `FlowAction`), which declares its inputs as serializable `Property` values and therefore survives the cache. ```kotlin // Legacy completion hook gradle.buildFinished { result -> val outcome = if (result.failure == null) "OK" else "FAILED" logger.lifecycle("Build outcome: $outcome") } ``` ## Related hooks `buildFinished` is the last of a family: `gradle.beforeProject`/`afterProject`, `gradle.projectsEvaluated`, `gradle.taskGraph.whenReady`. `buildFinished` is strictly the post-execution one.

  • Does the callback run if a task in the build fails?
    Yes. That is the point of it — it runs on both the success and failure paths, with result.failure populated on failure, so it is safe for teardown.
  • Why might buildFinished print a deprecation/problem warning in a modern build?
    Because it is incompatible with the configuration cache; Gradle reports it as a problem and recommends migrating to the Flow API.

saying these in an interview costs you the question

  • Saying it only runs on success — it runs on failure too.
  • Claiming buildFinished is configuration-cache compatible.
  • Confusing it with afterProject (which is per-project, during configuration).

context

open as a page

At a high level, what is the build configuration phase, and where do project evaluation hooks like afterEvaluate fit within it?

level: juniorimportance: must knowfreq 55%

basics

~20 s

The configuration phase is when Gradle reads every project's build script and registers tasks (but doesn't run them). Project evaluation hooks like afterEvaluate run at the tail of this phase, right after a project's script has been read.

open as a page

How does a BuildService perform cleanup at the end of a build, and what makes this a lifecycle hook?

level: middleimportance: must knowfreq 45%

basics

~10 s

Make the service implement AutoCloseable. Gradle calls close() automatically when the build finishes, so you can stop a server or flush a buffer there. You never call close() yourself.

open as a page

What is a Gradle BuildService, and why would you use one to hold state that is shared across tasks during a build?

level: middleimportance: must knowfreq 55%

basics

~20 s

A BuildService is an object Gradle creates lazily and shares across tasks in a build. It holds state (a counter, a connection pool) safely, lives for the whole build, and Gradle closes it at build end.

open as a page

What is project.afterEvaluate used for, and why would you defer configuration logic into it instead of writing it directly in the build script?

level: middleimportance: must knowfreq 70%

basics

~20 s

afterEvaluate registers a callback that runs after a project's build script has finished being evaluated. You use it when your logic depends on values (like extension properties) that are only set later in that script.

open as a page

Why is the Flow API (FlowScope/FlowAction) preferred over gradle.buildFinished, and how do you wire up a completion action with it?

level: seniorimportance: must knowfreq 40%

basics

~20 s

buildFinished captures live build objects, so it breaks the configuration cache. The Flow API replaces it: you inject FlowScope and FlowProviders, call scope.always(MyFlowAction::class) with serializable Property inputs, and the action runs at build completion in a cache-safe way.

open as a page

Why is OperationCompletionListener (via BuildEventsListenerRegistry) preferred over gradle.addListener for observing task execution in modern Gradle?

level: seniorimportance: must knowfreq 45%

basics

~10 s

OperationCompletionListener is registered through the injected BuildEventsListenerRegistry service and receives FinishEvents (including TaskFinishEvent) without holding mutable global state, so it stays compatible with the configuration cache — unlike gradle.addListener.

open as a page

What is a TaskExecutionListener in Gradle, and how do you register one to observe when tasks start and finish?

level: juniorimportance: should knowfreq 35%

basics

~10 s

A TaskExecutionListener gets a callback before each task runs (beforeExecute) and after it finishes (afterExecute). You register it via gradle.addListener(listener) in the build script or a plugin.

open as a page

When would you use a build-completion hook (buildFinished / FlowAction) versus task finalizedBy? Contrast their semantics.

level: middleimportance: should knowfreq 25%

basics

~20 s

Build-completion hooks run once at the end of the whole build, on success or failure — good for build-global teardown/reporting. finalizedBy attaches a finalizer task to a specific task and runs when that task runs — good for per-task cleanup like collecting reports after a test task.

open as a page

You start an external resource (a Docker container or temp DB) during a build and must guarantee it is torn down even if tasks fail. How would you implement that with a completion hook?

level: middleimportance: should knowfreq 30%

basics

~10 s

Register a completion hook that runs on the failure path too — legacy gradle.buildFinished { ... } or a config-cache-safe FlowScope.always FlowAction. Put the teardown there so it executes whether tasks pass or fail.

open as a page

What are the semantics of gradle.sharedServices.registerIfAbsent, and why is it the recommended way to register a BuildService from a plugin?

level: middleimportance: should knowfreq 28%

basics

~20 s

registerIfAbsent registers a named build service if one with that name doesn't already exist, and returns its Provider either way. That makes it idempotent, so multiple plugins can register the same service safely without conflicts.

open as a page

How do you react to the moment the task graph is fully built, before any task executes? When is that useful?

level: middleimportance: should knowfreq 40%

basics

~10 s

Use gradle.taskGraph.whenReady { graph -> ... } (or addTaskExecutionGraphListener with graphPopulated). It fires after configuration, once the full task graph is known but before execution starts, so you can inspect which tasks will run.

open as a page

Explain gradle.beforeProject and gradle.afterProject. How do they differ from a project calling its own afterEvaluate?

level: middleimportance: should knowfreq 45%

basics

~10 s

gradle.beforeProject and gradle.afterProject register callbacks that fire around the evaluation of EVERY project in the build, receiving the project as an argument. afterEvaluate is scoped to the single project that registers it.

open as a page

How would you emit a custom build report or CI notification at the end of every build across a developer team, and where would you register that completion logic?

level: seniorimportance: should knowfreq 22%

basics

~10 s

Register completion logic in an init script (~/.gradle/init.gradle.kts or distributed via an init plugin) so it applies to every build. Use a FlowAction reading buildWorkResult to report pass/fail to CI or a dashboard, config-cache-safe.

open as a page

What does Task.usesService(provider) do, and why is it needed when a task relies on a BuildService for shared state?

level: seniorimportance: should knowfreq 35%

basics

~10 s

usesService tells Gradle a task depends on that service. It keeps the service alive while the task runs and lets Gradle order/limit access correctly. Without it, parallel cleanup or constraints can misbehave.

open as a page

A plugin author uses a static field / Project extension to share a counter across tasks and it breaks under --parallel and the configuration cache. Why, and how does a BuildService fix it?

level: seniorimportance: should knowfreq 30%

basics

~10 s

Static/global state isn't safe across parallel tasks or worker processes and the configuration cache forbids capturing live build-script objects. A BuildService is a Gradle-managed, build-scoped, shareable holder that works under both.

open as a page

How would you implement a build-wide per-task timing report, and what pitfalls arise under parallel execution?

level: seniorimportance: should knowfreq 30%

basics

~10 s

Implement an OperationCompletionListener whose onFinish handles TaskFinishEvent — each event already carries start and end times, so you record duration per task path. Aggregate in a thread-safe BuildService since tasks run in parallel.

open as a page

Why is afterEvaluate considered problematic with the configuration cache, and what should you do instead?

level: seniorimportance: should knowfreq 38%

basics

~20 s

afterEvaluate runs eager configuration logic that often captures live build state. The configuration cache stores the configured graph and skips re-running configuration, so eager hooks become fragile. Prefer lazy Provider/Property wiring that resolves at execution time.

open as a page

When would you use gradle.projectsEvaluated, and how does it solve cross-project configuration ordering problems?

level: seniorimportance: should knowfreq 40%

basics

~20 s

gradle.projectsEvaluated fires once, after EVERY project in the build has been evaluated. Use it when configuration in one project depends on settings declared in another, so you must wait until all build scripts are read.

open as a page

What kinds of objects can gradle.addListener accept, and how does Gradle decide which events it receives?

level: middleimportance: nice to knowfreq 22%

basics

~10 s

gradle.addListener takes one object and inspects which listener interfaces it implements — e.g. TaskExecutionListener, TaskExecutionGraphListener, ProjectEvaluationListener, BuildListener — and routes the matching events to it. One object can implement several.

open as a page