skip to content

BuildService & Shared State

BuildService with typed parameters, registered once and shared across tasks, with constrained parallelism and automatic shutdown. Interviewers ask because it is the only sanctioned way to hold mutable state in a modern build.

on this pageshow

questions

6

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

level: middleimportance: must knowfreq 45%

answer

  1. Params extends BuildServiceParameters
  2. managed interface of Property
  3. parameters field at runtime
  4. BuildServiceParameters.None
  5. registerIfAbsent idempotent by name

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.

solid answer

~30 s

A parameterized service implements `BuildService<P>` where `P extends BuildServiceParameters` — typically a nested interface of `Property`/`Provider`-typed members (a managed type, so Gradle implements it). At runtime the service reads them via the `parameters` field. You register with `gradle.sharedServices.registerIfAbsent("name", MyService::class) { parameters.someProp.set(value) }`; the configuration action's receiver exposes `parameters` and the spec (`maxParallelUsages`). Because params are wired through lazy `Provider`s, values can be deferred and are serialized by the configuration cache, keeping the service config-cache compatible. If a service needs no params, use the built-in `BuildServiceParameters.None`.

code

kotlin · 10 lines
kotlin
abstract class GreetService : BuildService<GreetService.Params> {
    interface Params : BuildServiceParameters {
        val greeting: Property<String>
    }
    fun greet(name: String) = parameters.greeting.get() + ", " + name
}

val svc = gradle.sharedServices.registerIfAbsent("greet", GreetService::class) {
    parameters.greeting.set("Hello")
}

go deeper

for a junior

Knows params are a nested interface with Property fields set during registration.

for a middle

Explains managed types, BuildServiceParameters.None, and reading via the parameters field.

for a senior

Connects Provider-based params to configuration-cache serialization and idempotent registration.

for a principal

Sets conventions: keep params declarative/serializable, build live resources internally, standardize service naming to avoid collisions across plugins.

## Parameters are a managed type A BuildService receives its configuration through a **parameters** object whose type extends `BuildServiceParameters`. You usually declare it as a **nested interface** with abstract `Property<T>` / `ListProperty<T>` / `Provider<T>` accessors. Gradle generates the implementation (a 'managed type'), so you never write a constructor or backing fields for params. ```kotlin abstract class HttpClientService : BuildService<HttpClientService.Params>, AutoCloseable { interface Params : BuildServiceParameters { val baseUrl: Property<String> val timeoutMillis: Property<Long> } private val client = buildClient( parameters.baseUrl.get(), parameters.timeoutMillis.getOrElse(30_000) ) fun get(path: String): String = client.get(path) override fun close() = client.close() } ``` The abstract `parameters` property is injected by Gradle and returns the typed params object. ## Registration supplies the values ```kotlin val http = gradle.sharedServices.registerIfAbsent("http", HttpClientService::class) { parameters.baseUrl.set("https://example.com") parameters.timeoutMillis.set(10_000) maxParallelUsages.set(4) } ``` The lambda's receiver is a `BuildServiceSpec<Params>` exposing: - **`parameters`** — set the managed property values (lazily; you can pass other `Provider`s). - **`maxParallelUsages`** — cap concurrency (see the dedicated question). ## No-parameter services When a service needs no configuration, parameterize it with the built-in **`BuildServiceParameters.None`**: ```kotlin abstract class Counter : BuildService<BuildServiceParameters.None> { /* ... */ } ``` ## Why Provider-based params matter Because params are `Property`/`Provider` values, they participate in lazy evaluation and are **serialized by the configuration cache**. This is what lets a service survive a cached configuration phase: the params snapshot is stored and replayed, and the service is reconstructed on cache hit. Putting non-serializable mutable objects directly into params defeats this — keep params to serializable, declarative inputs and build live resources inside the service body. ## `registerIfAbsent` semantics The first argument is a **name** acting as a registration key. Calling `registerIfAbsent` again with the same name returns the existing `Provider` and does **not** re-run the config action — this makes registration idempotent across plugins that might both want the service.

  • What happens if two plugins both call registerIfAbsent with the same name?
    The second call returns the already-registered Provider and skips the config action — registration is idempotent by name, so the service is shared, not duplicated.
  • Why should params be Property/Provider types rather than plain fields?
    So they're lazy and serializable by the configuration cache, letting the service's configuration be cached and replayed.

saying these in an interview costs you the question

  • Writing a constructor to receive params — params come from the managed parameters field, not constructor args.
  • Stuffing live, non-serializable objects (sockets, threads) into params instead of constructing them inside the service.

context

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

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

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