skip to content

Lifecycle and Task Listeners

Registering task and operation listeners to observe execution events as they happen. Interviewers ask because listeners are how build telemetry is collected, and why many of them are now discouraged.

on this pageshow

questions

5

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

level: seniorimportance: must knowfreq 45%

answer

  1. OperationCompletionListener.onFinish(FinishEvent)
  2. TaskFinishEvent / TaskOperationResult (fromCache, upToDate)
  3. BuildEventsListenerRegistry.onTaskCompletion(provider)
  4. wired via shared BuildService
  5. configuration-cache compatible

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.

solid answer

~40 s

Legacy hooks like `gradle.addListener(TaskExecutionListener)` and `taskGraph.afterTask` carry live, mutable, build-scoped state and are flagged **incompatible with the configuration cache**, because the cache serializes the configured model and replays it without re-running configuration callbacks. The Tooling-API path solves this: you implement `org.gradle.tooling.events.OperationCompletionListener`, whose `onFinish(FinishEvent)` receives typed events — notably `TaskFinishEvent` with a `TaskOperationResult` (success/failure/skipped/from-cache). You register it by injecting `BuildEventsListenerRegistry` (a Gradle service) into your plugin or a `BuildService`, then calling `registry.onTaskCompletion(provider)`, where the provider yields a `BuildService` implementing the listener. Because the listener lives in a shared `BuildService` and is wired through a `Provider`, Gradle can manage its lifecycle and keep it cache-safe. This is the supported way to build profiling, metrics, or build-scan-style observers today.

code

kotlin · 8 lines
kotlin
abstract class Listener : BuildService<BuildServiceParameters.None>, OperationCompletionListener {
    override fun onFinish(event: FinishEvent) {
        if (event is TaskFinishEvent) println(event.descriptor.taskPath)
    }
}
@get:Inject abstract val registry: BuildEventsListenerRegistry
val svc = gradle.sharedServices.registerIfAbsent("l", Listener::class.java) {}
registry.onTaskCompletion(svc)

go deeper

for a junior

Likely beyond junior scope; at most know that a newer, cache-friendly listener API exists.

for a middle

Name OperationCompletionListener and BuildEventsListenerRegistry and that they replace gradle.addListener for cache compatibility.

for a senior

Explain the full wiring through a BuildService, the typed TaskFinishEvent/result, and why the indirection is what makes it cache-safe.

for a principal

Set org policy: profiling/metrics listeners ship as cache-safe BuildServices in convention plugins; ban gradle.addListener in shared build logic to protect configuration-cache adoption.

## The compatibility problem The **configuration cache** lets Gradle skip the configuration phase on subsequent builds by serializing the configured task graph and reloading it. Anything registered as a *live build-scoped callback* — `gradle.addListener(...)`, `taskGraph.afterTask { }`, `buildFinished { }` — can't be serialized meaningfully and is reported as a configuration-cache problem. So the old `TaskExecutionListener` approach blocks one of Gradle's biggest performance features. ## The cache-safe replacement Gradle's Tooling-API events provide a managed, cache-friendly observation channel: - `OperationCompletionListener` (interface) with `onFinish(FinishEvent event)`. - Events arrive typed: `TaskFinishEvent` for tasks, carrying a `TaskOperationResult` that is a `TaskSuccessResult`, `TaskFailureResult`, `TaskSkippedResult`, etc. From a success result you can read `isFromCache()` and `isUpToDate()`. - Registration goes through the injected **`BuildEventsListenerRegistry`** service: `registry.onTaskCompletion(listenerProvider)`. ## Wiring it through a BuildService The listener is supplied as a `Provider<out OperationCompletionListener>`, almost always a **shared `BuildService`** (`gradle.sharedServices.registerIfAbsent(...)`). The service holds any aggregated state (timings, counts) in a thread-safe way and survives across the build; Gradle manages its lifecycle and disposes it at build end. This indirection is exactly what makes it cache-safe: no global mutable listener is captured during configuration. ```kotlin abstract class TimingListener : BuildService<BuildServiceParameters.None>, OperationCompletionListener, AutoCloseable { override fun onFinish(event: FinishEvent) { if (event is TaskFinishEvent) { val ms = (event.result.endTime - event.result.startTime) println("${event.descriptor.taskPath} -> ${ms}ms") } } override fun close() { /* flush metrics */ } } abstract class TimingPlugin : Plugin<Project> { @get:Inject abstract val registry: BuildEventsListenerRegistry override fun apply(project: Project) { val svc = project.gradle.sharedServices.registerIfAbsent( "timing", TimingListener::class.java) {} registry.onTaskCompletion(svc) } } ``` ## What the events give you From `TaskFinishEvent` you get the task path, start/end time, and a result you can pattern-match: was it a failure, skipped, up-to-date, or served from the build cache. That's enough to build per-task timing, failure auditing, or cache-hit-rate reporting — the same things people used `afterExecute` for — but compatible with the configuration cache and the parallel/worker execution model. ## When the legacy API is still acceptable If a build deliberately doesn't use the configuration cache, `taskGraph.afterTask`/`TaskExecutionListener` still works and is simpler. But for any new shared plugin or a build that wants cache benefits, `OperationCompletionListener` is the correct choice. ## Summary `gradle.addListener` = simple, global, mutable, **cache-incompatible**. `OperationCompletionListener` + `BuildEventsListenerRegistry` + `BuildService` = managed, typed, thread-safe, **cache-compatible** — the modern standard.

  • Why must the listener be supplied as a Provider/BuildService rather than a plain object?
    So Gradle controls its instantiation and lifecycle and can keep it out of the serialized configuration model — that indirection is what makes the registration configuration-cache compatible and lets the service hold shared state safely under parallel execution.
  • How do you tell from a TaskFinishEvent whether the result came from the build cache?
    Inspect event.result: if it's a TaskSuccessResult, call isFromCache() (and isUpToDate()) to distinguish a cache hit, an up-to-date no-op, and a real execution.
  • Does onFinish fire for non-task operations?
    Yes — OperationCompletionListener can receive other FinishEvent subtypes; you typically guard with `if (event is TaskFinishEvent)` to handle only tasks.

saying these in an interview costs you the question

  • Saying gradle.addListener is configuration-cache compatible — it is not.
  • Registering the listener as a captured local object instead of a BuildService/Provider, defeating the cache-safety.
  • Reading TaskState from these Tooling-API events — they expose TaskOperationResult, not TaskState.

context

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

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

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

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