skip to content

BuildService For Shared State

Replacing configuration-cache-incompatible shared state with a BuildService that can be carried safely across executions. Asked as the standard fix once a build trips over Project access at execution time.

on this pageshow

questions

5

Walk through registering a BuildService with gradle.sharedServices.registerIfAbsent and passing parameters. Why is registerIfAbsent preferred over a plain register, and what does it return?

level: middleimportance: must knowfreq 45%

answer

  1. gradle.sharedServices.registerIfAbsent(name, Type){}
  2. returns Provider<MyService>, lazy
  3. idempotent → reuse across plugins
  4. Params extends BuildServiceParameters, serializable
  5. Params.None when no config

basics

~10 s

Call gradle.sharedServices.registerIfAbsent("name", MyService::class) { parameters { ... } }. It returns a Provider<MyService>. registerIfAbsent is idempotent, so registering the same name twice (e.g. from two plugins) reuses one instance instead of failing.

solid answer

~40 s

You register against `gradle.sharedServices`: `registerIfAbsent(name, MyServiceType) { parameters { x.set(...) } }`. The service type is an abstract class extending `BuildService<P>` where `P` extends `BuildServiceParameters`; Gradle implements the abstract `getParameters()`. The call returns a **`Provider<MyService>`** — lazy, so the service isn't created until a task consumes it. **`registerIfAbsent`** is preferred because registration is keyed by name: if another plugin (or your own code running twice) registers the same name, you get the **already-registered instance** instead of a duplicate-registration error. Parameters are how you pass immutable configuration into the service (paths, flags, a `RegularFileProperty` output) — they must themselves be config-cache serializable. The returned provider is what you wire into each task's `Property<MyService>`.

code

kotlin · 14 lines
kotlin
interface DownloadParams : BuildServiceParameters {
    val server: Property<String>
}

abstract class DownloadService : BuildService<DownloadParams>, AutoCloseable {
    private val client = java.net.http.HttpClient.newHttpClient()
    fun base() = parameters.server.get()
    override fun close() {}
}

val dl = gradle.sharedServices.registerIfAbsent("download", DownloadService::class) {
    parameters { server.set("https://repo.example.com") }
    maxParallelUsages.set(2)
}

go deeper

for a junior

Recall the call shape: registerIfAbsent(name, Type){ parameters{} } returning a Provider.

for a middle

Explain idempotency, laziness, the abstract service + Params pattern, and serializable params.

for a senior

Cover maxParallelUsages throttling and cross-plugin sharing semantics keyed by name.

for a principal

Define naming conventions so independent plugins converge on one shared service rather than fragmenting state.

## The registration API Build services are registered through the **`BuildServiceRegistry`** exposed as `project.gradle.sharedServices`. The primary entry point is: ```kotlin val provider: Provider<MyService> = gradle.sharedServices.registerIfAbsent("myService", MyService::class) { parameters { // configure the service's Params object } maxParallelUsages.set(1) // optional concurrency limit } ``` ### The service type and its Params A build service is an **abstract class** implementing `BuildService<P>`: ```kotlin interface MyParams : BuildServiceParameters { val rootDir: DirectoryProperty val verbose: Property<Boolean> } abstract class MyService : BuildService<MyParams>, AutoCloseable { // getParameters() is implemented by Gradle override fun close() { /* release resources */ } } ``` Gradle generates the implementation of `getParameters()`, so inside the service you read configuration via `parameters.rootDir.get()`. If the service needs no parameters, use `BuildServiceParameters.None`. ### Why `registerIfAbsent` over `register` Registration is **keyed by the name string**. The older `register(...)` fails if the name is already taken. `registerIfAbsent`: - **Is idempotent** — calling it again with the same name returns the *existing* registration's provider. This matters because plugins are applied independently; two plugins both wanting a shared HTTP client or a shared counter can each call `registerIfAbsent("http", ...)` and end up sharing **one** instance. - Avoids accidental duplicate-registration failures in multi-plugin or multi-subproject builds. ### What it returns and laziness It returns a **`Provider<MyService>`**. The provider is lazy: the service is **not instantiated at registration time**. Gradle creates the single instance the first time a task actually calls `provider.get()` (or the service is injected). This laziness is what keeps registration cheap and config-cache friendly. ### Parameters must be serializable Whatever you put in the Params object is captured as part of the configuration cache, so it must be a config-cache-supported type — Gradle managed properties (`Property`, `RegularFileProperty`, `DirectoryProperty`, `ListProperty`, etc.) are ideal because they're lazy and serializable. Don't stuff live `Project` or `Task` references into Params. ### Optional knobs - `maxParallelUsages` limits how many tasks may use the service concurrently — useful to throttle access to a constrained external resource. - `parameters { }` is omitted (or empty) for `BuildServiceParameters.None`.

  • What type does registerIfAbsent return and when is the service actually created?
    It returns a Provider<MyService>. The instance is created lazily the first time a task consumes the provider (calls get() or has it injected), not at registration time.
  • Two plugins both call registerIfAbsent with name 'http'. What happens?
    They share a single instance — the second call returns the provider for the already-registered service rather than failing, which is the main reason to prefer registerIfAbsent over register.

saying these in an interview costs you the question

  • Saying registration eagerly creates the service — it's lazy via the Provider.
  • Putting live Project/Task references into the Params object, which breaks config-cache serialization.

context

open as a page

Why does enabling the configuration cache break code that holds shared mutable state (e.g. a static counter or a shared object referenced by tasks), and how does a BuildService solve it?

level: middleimportance: must knowfreq 55%

basics

~20 s

The configuration cache serializes the task graph and reuses it, so plain shared objects or statics aren't re-created or shared correctly across tasks and parallel workers. A BuildService is the supported holder for shared state that the config cache understands and reuses safely.

open as a page

A BuildService is shared across tasks running in parallel. What guarantees does Gradle give about its instance and lifecycle, and what concurrency responsibilities remain yours?

level: seniorimportance: should knowfreq 35%

basics

~20 s

Gradle creates exactly one instance per build, lazily, and closes it (if AutoCloseable) when the build ends. It does NOT synchronize your methods, so any mutable internal state must be made thread-safe by you. maxParallelUsages can throttle concurrent users.

open as a page

How do you make a task depend on a BuildService so it's tracked as a dependency, and what does @ServiceReference add over manually wiring usesService?

level: seniorimportance: should knowfreq 38%

basics

~20 s

Expose an abstract Property<MyService> on the task and either set it from the provider plus call usesService(provider), or annotate it with @ServiceReference("name") so Gradle auto-wires and registers the usage. @ServiceReference removes the manual wiring boilerplate.

open as a page

You inherit a plugin that uses a static field and a captured Project reference to accumulate build-wide data, and it fails with configuration cache enabled. Outline the migration to a BuildService.

level: principalimportance: should knowfreq 28%

basics

~20 s

Move the static state into an abstract BuildService<Params>, pass any needed config (paths, flags) via Params instead of capturing Project, register it with registerIfAbsent, and have each task reference it via @ServiceReference. Remove statics and execution-time Project access.

open as a page