skip to content

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%

answer

  1. one instance per build, shared across tasks
  2. gradle.sharedServices.registerIfAbsent → Provider
  3. lazy creation on first use
  4. replaces global/static build state
  5. AutoCloseable → close() at build end

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.

solid answer

~40 s

A `BuildService` is a holder of state or resources that Gradle instantiates **once per build** and shares across all tasks that ask for it, even when those tasks run in parallel. You declare it as a class extending `BuildService<Params>` and register it through `gradle.sharedServices.registerIfAbsent(name, MyService::class.java) {}`, which returns a `Provider<MyService>`. Tasks reference that provider, so the service is created lazily on first use, not at configuration time. Because Gradle manages its single instance and its lifecycle, it is the sanctioned replacement for build-scoped global/static state (which breaks parallelism and the configuration cache). The instance is reused across tasks but each project/build invocation gets its own — it is **not** a cross-build singleton. At build completion Gradle disposes it, calling `close()` if it implements `AutoCloseable`.

code

kotlin · 12 lines
kotlin
abstract class CounterService : BuildService<BuildServiceParameters.None> {
    private val n = java.util.concurrent.atomic.AtomicInteger()
    fun inc() = n.incrementAndGet()
    fun total() = n.get()
}

val counter = gradle.sharedServices.registerIfAbsent("counter", CounterService::class.java) {}

tasks.register("work") {
    val svc = counter // capture the Provider
    doLast { svc.get().inc() }
}

go deeper

for a junior

Know it is a shared object created once per build that tasks can use, and Gradle cleans it up at the end.

for a middle

Explain lazy creation via the returned Provider, the cross-task single-instance sharing, and that it replaces unsafe global/static state.

for a senior

Connect it to parallel execution and configuration-cache constraints; discuss thread-safety of the service's own internal state.

for a principal

Frame it as the org-wide sanctioned pattern for build-scoped shared resources and how it removes hidden global state that blocks parallel/cached builds.

## The problem BuildService solves In a Gradle build, many tasks may need to **share one piece of state** — a web-server handle, a database connection, a counter of processed files, an in-memory cache. The naive approach is a global/static variable or a property hung off the `Project`. That breaks for three reasons: tasks can run **in parallel** (so the state needs to be thread-safe), Gradle may run tasks across **multiple worker processes**, and the **configuration cache** forbids tasks from capturing arbitrary live objects from the build script. `BuildService` is Gradle's first-class answer. It is a managed object with a **build-scoped lifecycle**: created lazily the first time a task actually uses it, shared by every task that references it, and torn down when the build ends. ## Anatomy A service is a class: ```kotlin abstract class CounterService : BuildService<BuildServiceParameters.None> { private val count = java.util.concurrent.atomic.AtomicInteger() fun increment() = count.incrementAndGet() fun total() = count.get() } ``` You register it on `gradle.sharedServices`: ```kotlin val counter = gradle.sharedServices.registerIfAbsent("counter", CounterService::class.java) {} ``` `registerIfAbsent` returns a `Provider<CounterService>`. **Registration does not create the instance** — it is created lazily the first time the provider is queried by a running task. `registerIfAbsent` is idempotent: register the same name twice and you get the same registration back, which is why it is safe to call from many plugins. ## Why "shared state" specifically The key property for this lifecycle role is **one instance per build, reused across tasks**. Task A can `increment()`, task B can read `total()`, and they see the same object — Gradle does not give each task its own copy. Because the instance can be touched by parallel tasks, the service's own internal state must be thread-safe (note the `AtomicInteger` above). ## Lifecycle / shutdown If the service implements `AutoCloseable`, Gradle calls `close()` automatically at the **end of the build** — this is the lifecycle-hook aspect. That makes it the right place to stop a started server, flush a buffer, or print a summary. You never call `close()` yourself. ## Relationship to other concepts The full authoring surface (parameters, `getMaxParallelUsages`, `usesService`) and the configuration-cache migration story are separate topics; here the point is the **build-scoped, cross-task, auto-disposed shared-state** behavior.

  • When exactly is the service instance created?
    Lazily — the first time a running task actually calls `.get()` on the provider, not at registration/configuration time.
  • Is a BuildService a singleton shared across separate Gradle invocations?
    No. It is scoped to a single build invocation. Each build gets a fresh instance; it is not persisted across runs.

Think of it as a shared whiteboard for the duration of one build meeting: everyone (every task) writes to and reads from the same board, and the janitor (Gradle) wipes it clean when the meeting ends.

saying these in an interview costs you the question

  • Saying the service is created eagerly at configuration time (it is lazy).
  • Claiming it persists state across separate builds (it is per-build).
  • Thinking each task gets its own copy of the service.

context