skip to content

Concurrency & Build Services

Doing work in parallel inside a task with the Worker API, choosing an isolation level, and sharing state safely through build services. Interviewers ask because this is the correct answer to 'my plugin needs a thread pool'.

on this pageshow

explore

questions

21

What is the Gradle Worker API, and why would a custom task use it instead of just running its work directly in the task action?

level: juniorimportance: must knowfreq 55%

answer

  1. @Inject WorkerExecutor
  2. WorkAction<WorkParameters>
  3. queue from noIsolation/classLoaderIsolation/processIsolation
  4. submit returns immediately
  5. Gradle awaits before task done

basics

~20 s

The Worker API lets a task hand units of work to Gradle's worker pool so they run in parallel. You inject WorkerExecutor, define a WorkAction, and submit work to a queue. It enables parallelism and isolation that a plain task action can't get.

solid answer

~40 s

The Worker API is Gradle's mechanism for splitting a task's work into discrete units (`WorkAction`s) submitted to a `WorkerExecutor` queue. Gradle runs those units in parallel using its build-wide worker thread/process pool, respecting `--max-workers`. Compared to running everything inline in `@TaskAction`, it gives you: (1) parallel execution of independent items even within one task; (2) isolation — `classLoaderIsolation` or `processIsolation` separate your code/deps from Gradle and other tasks; (3) better resource control because Gradle coordinates the worker budget across all running tasks. You inject `WorkerExecutor` via `@Inject`, get a queue from `noIsolation()`/`classLoaderIsolation()`/`processIsolation()`, and `submit(MyAction::class) { params... }`. The task action returns immediately; Gradle ensures the work completes before the task is done.

code

kotlin · 25 lines
kotlin
abstract class ReverseTask : DefaultTask() {
    @get:Inject abstract val workerExecutor: WorkerExecutor
    @get:InputFiles abstract val sources: ConfigurableFileCollection
    @get:OutputDirectory abstract val outputDir: DirectoryProperty

    @TaskAction
    fun run() {
        val queue = workerExecutor.noIsolation()
        sources.forEach { f ->
            queue.submit(ReverseAction::class.java) {
                it.inputFile.set(f)
                it.outputDir.set(outputDir)
            }
        }
    }
}

interface ReverseParams : WorkParameters {
    val inputFile: RegularFileProperty
    val outputDir: DirectoryProperty
}

abstract class ReverseAction : WorkAction<ReverseParams> {
    override fun execute() { /* do the work */ }
}

go deeper

for a junior

Know the three pieces: inject WorkerExecutor, implement WorkAction, submit to a queue for parallel work.

for a middle

Explain the parameters-via-WorkParameters mechanism and that submit is non-blocking with Gradle awaiting automatically.

for a senior

Contrast inline task work vs Worker API in terms of parallelism, isolation, and respecting the build-wide worker budget.

for a principal

Frame it as the supported concurrency primitive for plugins — why Gradle owns the pool, and how it interacts with --max-workers and the configuration cache.

## What the Worker API is The **Worker API** is Gradle's supported way to run a task's work as independent **units of work** on a shared worker pool, instead of doing it all inline inside a `@TaskAction` method. It is the recommended replacement for spinning up your own threads/executors inside a task — Gradle owns the thread/process budget and coordinates it across the whole build. Two core types: - **`WorkerExecutor`** — a service Gradle injects into your task. You ask it for a **`WorkQueue`** with a chosen isolation mode. - **`WorkAction<P : WorkParameters>`** — an interface you implement with a single `execute()` method. It is the unit of work. Its parameters arrive via a typed `WorkParameters` object. ## Why not just run inline? A plain task action runs serially on the task's own thread. The Worker API adds three things: 1. **Parallelism within a task.** If a task processes 200 files, each file can be a `WorkAction` and Gradle runs them across worker threads up to `--max-workers`. 2. **Isolation.** `classLoaderIsolation()` runs the action in an isolated classloader (its own classpath, no leakage of Gradle internals); `processIsolation()` runs it in a separate JVM (own heap, JVM args). This is how, e.g., annotation processors or external tools run without polluting the build JVM. 3. **Coordinated resource use.** Because all tasks submit into the same pool, Gradle keeps total concurrency within the worker limit instead of every task spawning its own threads. ## The flow ```kotlin abstract class ReverseTask : DefaultTask() { @get:Inject abstract val workerExecutor: WorkerExecutor @get:InputFiles abstract val sources: ConfigurableFileCollection @get:OutputDirectory abstract val outputDir: DirectoryProperty @TaskAction fun run() { val queue = workerExecutor.noIsolation() sources.forEach { f -> queue.submit(ReverseAction::class.java) { it.inputFile.set(f) it.outputDir.set(outputDir) } } // task action returns; Gradle awaits the queue before the task finishes } } interface ReverseParams : WorkParameters { val inputFile: RegularFileProperty val outputDir: DirectoryProperty } abstract class ReverseAction : WorkAction<ReverseParams> { override fun execute() { val src = parameters.inputFile.asFile.get() // ... do the work } } ``` Key points: the parameters interface uses Gradle's lazy types (`Property`, `RegularFileProperty`, etc.); the `WorkAction` must be `abstract` (Gradle generates the `parameters` accessor); and `submit` returns immediately — the task action does not block on each unit. ## When the work completes Work submitted to a queue is asynchronous from the caller's view, but Gradle guarantees all submitted units finish before the **task** is considered complete — you don't normally need to wait explicitly. (You can force a wait mid-action with `await()`, covered separately.)

  • Does submitting to a no-isolation queue actually run work in parallel?
    Yes — even noIsolation work runs on Gradle's worker threads and can execute concurrently with other submitted units, up to --max-workers. The only thing noIsolation skips is classloader/process separation, not parallelism.
  • How do parameters get into the WorkAction?
    Through a typed interface extending WorkParameters whose properties are Gradle lazy types. You set them in the submit { } block; Gradle serializes/snapshots them and exposes them via the action's generated parameters property.

Think of the task action as a foreman handing slips of work to a shared crew (Gradle's worker pool) rather than doing every job personally — the crew is sized once for the whole site, not per foreman.

saying these in an interview costs you the question

  • Saying the Worker API is only for separate processes — noIsolation and classLoaderIsolation also exist and run in-JVM.
  • Claiming you must call await() to make work run — Gradle awaits the queue automatically before the task finishes.
  • Spawning raw Java threads/ExecutorService inside a task instead of using the worker pool.

context

open as a page

How do you define and register a BuildService with parameters, and how are those parameters supplied?

level: middleimportance: must knowfreq 45%

basics

~10 s

Define a nested Params interface extending BuildServiceParameters with Property fields, then register with registerIfAbsent and set those Property values inside the configuration action.

open as a page

What is a Gradle BuildService, and what problem does it solve?

level: middleimportance: must knowfreq 55%

basics

~10 s

A BuildService is a shared, build-scoped object that holds state or resources (caches, connections) and can be safely shared across tasks, including tasks running in parallel.

open as a page

The Gradle Worker API offers three isolation modes — noIsolation, classLoaderIsolation, and processIsolation. What does each one isolate, and how do they differ?

level: middleimportance: must knowfreq 55%

basics

~10 s

noIsolation runs work in the Gradle JVM with the buildscript classpath. classLoaderIsolation runs it in the same JVM but under a separate classloader from a custom classpath. processIsolation runs it in a forked JVM.

open as a page

What does the @ServiceReference annotation do on a Gradle task, and why is it preferable to manually wiring a build service?

level: middleimportance: must knowfreq 45%

basics

~10 s

@ServiceReference marks an abstract Property<MyService> on a task so Gradle auto-injects a registered shared build service and automatically tracks it as a task dependency, so you don't call usesService() yourself.

open as a page

How do you pass data into a WorkAction, and what are the constraints on those parameters?

level: middleimportance: must knowfreq 45%

basics

~20 s

Define an interface extending WorkParameters with Gradle lazy-typed properties (Property, RegularFileProperty, etc.). Set them in the submit { } block. The action reads them via its parameters property. Values must be isolatable so Gradle can snapshot them.

open as a page

Explain the difference between noIsolation(), classLoaderIsolation(), and processIsolation() when obtaining a WorkQueue. When would you choose each?

level: seniorimportance: must knowfreq 50%

basics

~20 s

noIsolation runs in the build JVM with the plugin's classpath. classLoaderIsolation runs in a separate classloader so deps don't leak. processIsolation runs in a forked JVM with its own classpath, heap, and JVM args. More isolation costs more startup overhead.

open as a page

When using processIsolation, how do you configure the forked worker JVM — for example its heap size and JVM arguments — and what API exposes those settings?

level: middleimportance: should knowfreq 40%

basics

~10 s

Call workerExecutor.processIsolation { forkOptions { ... } }. forkOptions is a JavaForkOptions where you set maxHeapSize, minHeapSize, jvmArgs, systemProperty, environment, and the working directory of the forked JVM.

open as a page

Why does noIsolation use the buildscript classpath, and what risk does that create when your worker action depends on a library Gradle also ships?

level: middleimportance: should knowfreq 30%

basics

~20 s

noIsolation runs the action on the plugin/buildscript classloader, so you cannot pick a custom classpath. If your action needs a different version of a library Gradle bundles, you get the version Gradle exposes — leading to conflicts or wrong behavior.

open as a page

When consuming a build service via @ServiceReference, why and how should a task guard against the service being absent?

level: middleimportance: should knowfreq 20%

basics

~10 s

A named @ServiceReference may match no registered service, leaving the property unset. Pair it with @Optional and check counter.isPresent (or use orElse) before calling .get(), so the task fails gracefully instead of throwing.

open as a page

How does the Worker API surface failures from a WorkAction, and how should a plugin author handle errors across submitted units?

level: middleimportance: should knowfreq 30%

basics

~20 s

An exception thrown in a WorkAction.execute() is captured by Gradle and rethrown to the build as a WorkerExecutionException, failing the task. With multiple failing units, Gradle aggregates them. The failure surfaces at await() if you call it, otherwise at task end.

open as a page

Walk through the lifecycle of a BuildService that holds an expensive resource, including AutoCloseable cleanup and thread-safety expectations.

level: seniorimportance: should knowfreq 28%

basics

~20 s

The service is created lazily on first use, shared as a single instance for the build, and if it implements AutoCloseable, Gradle calls close() once at build end. Because tasks may use it in parallel, its methods must be thread-safe.

open as a page

What does getMaxParallelUsages() control on a BuildService, and how is it enforced?

level: seniorimportance: should knowfreq 35%

basics

~10 s

It caps how many tasks may use the service at the same time. Set maxParallelUsages at registration to throttle access to a constrained resource; Gradle limits concurrent users to that number.

open as a page

Why must a task call usesService() (or use @ServiceReference) instead of just calling provider.get(), and what breaks if it doesn't?

level: seniorimportance: should knowfreq 30%

basics

~10 s

Declaring usage tells Gradle the task depends on the service so it manages lifecycle and concurrency. Without it, maxParallelUsages isn't enforced and the configuration cache may warn or fail.

open as a page

Why must WorkAction parameters be isolatable/serializable, and how does the requirement change across the three isolation modes?

level: seniorimportance: should knowfreq 25%

basics

~20 s

Gradle snapshots (isolates) work parameters when you submit, so each unit gets an independent copy. For processIsolation the parameters must cross to a forked JVM, so they must be serializable. Use Property/managed types, not arbitrary mutable objects.

open as a page

You are deciding between classLoaderIsolation and processIsolation for a worker-based task. What concrete factors push you from one to the other?

level: seniorimportance: should knowfreq 35%

basics

~20 s

Use classLoaderIsolation when you only need a separate classpath (version isolation) and the work is well-behaved. Move to processIsolation when work needs its own heap/JVM args, may call System.exit, may crash natively, or must not touch daemon memory.

open as a page

How does Gradle resolve which registered build service to inject for a @ServiceReference, with and without an explicit name?

level: seniorimportance: should knowfreq 30%

basics

~10 s

Without a name, Gradle injects the single registered service whose type matches the property's type. With a name, it matches the registration name; if none matches, the property stays undefined rather than failing.

open as a page

How do you implement a build-listener build service that observes task execution events via OperationCompletionListener, and how is it registered?

level: seniorimportance: should knowfreq 22%

basics

~10 s

Make a BuildService also implement OperationCompletionListener, override onFinish(event) to inspect TaskFinishEvent results, then register it with BuildEventsListenerRegistry.onTaskCompletion(provider). This is the configuration-cache-safe replacement for legacy TaskExecutionListener.

open as a page

Why does Gradle need to know a task 'uses' a build service, and how does @ServiceReference satisfy that requirement?

level: seniorimportance: should knowfreq 28%

basics

~10 s

Gradle throttles concurrent tasks against a service's maxParallelUsages limit, but only for tasks that declare they use it. @ServiceReference implicitly declares that usage, so the limit and lifecycle tracking apply automatically.

open as a page

What does WorkQueue.await() do, when is it actually needed, and what are the downsides of calling it inside a task action?

level: seniorimportance: should knowfreq 35%

basics

~20 s

await() blocks the task action until all work submitted to that queue finishes, and rethrows any worker failures as a WorkerExecutionException. You rarely need it because Gradle awaits automatically at task end; use it only when later steps in the same action depend on the work's output.

open as a page

A legacy plugin keeps shared state (a counter and a cache) in static fields, breaking the configuration cache and parallel builds. How would you migrate it to a BuildService?

level: principalimportance: nice to knowfreq 18%

basics

~10 s

Move the static state into a BuildService implementation, register it with registerIfAbsent, make tasks declare it via @ServiceReference/usesService, and use thread-safe structures so it works under the configuration cache and parallel execution.

open as a page