skip to content

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%

answer

  1. one instance per build, lazy
  2. AutoCloseable.close() once at build end
  3. Gradle does NOT lock your methods
  4. you guard mutable state (Atomic*/Concurrent*)
  5. maxParallelUsages throttles users

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.

solid answer

~40 s

Gradle guarantees a **single instance** of the service for the whole build, created lazily on first use and **closed once** at build end if it implements `AutoCloseable`. That single-instance guarantee holds even with the configuration cache and across **parallel** task execution. What Gradle does **not** do is serialize access to your methods — multiple parallel tasks can call into the same instance concurrently, so any mutable field must be guarded yourself (`AtomicInteger`, `ConcurrentHashMap`, `synchronized`, etc.). If the service wraps a resource that tolerates only N concurrent users, set `maxParallelUsages.set(N)` at registration; Gradle then limits how many tasks hold it at once. Because state lives in this one managed instance and tasks reach it only through a tracked provider, it stays config-cache compatible — that's the whole reason it replaces incompatible shared/static state.

code

kotlin · 9 lines
kotlin
abstract class StatsService : BuildService<BuildServiceParameters.None>, AutoCloseable {
    private val counts = java.util.concurrent.ConcurrentHashMap<String, Int>()
    fun record(key: String) { counts.merge(key, 1, Int::plus) }
    override fun close() { println("totals: $counts") }
}

gradle.sharedServices.registerIfAbsent("stats", StatsService::class) {
    maxParallelUsages.set(4)
}

go deeper

for a junior

Know there is one instance per build and it closes at the end via AutoCloseable.

for a middle

Add that you must make internal state thread-safe and that creation is lazy.

for a senior

Discuss maxParallelUsages, deterministic close ordering, and config-cache reference serialization.

for a principal

Set guidance for resource-backed services (pooling, throttling, graceful close) and review for hidden races in shared build infrastructure.

## What Gradle guarantees 1. **Exactly one instance per build.** Regardless of how many tasks, plugins, or subprojects reference the service by name, Gradle instantiates it **once**. Registration is idempotent via `registerIfAbsent`. 2. **Lazy creation.** The instance is built the **first time a task actually uses it**, not at registration. This avoids paying for unused services and keeps configuration cheap. 3. **Deterministic teardown.** If the service implements `AutoCloseable`, Gradle calls `close()` **once**, after the last task that uses it has finished. This is where you release sockets, files, thread pools, etc. 4. **Config-cache compatibility.** The service's *reference* is what gets serialized into the configuration cache, not its live mutable state, so reuse across builds is correct. ## What stays your responsibility - **Thread safety.** Gradle shares the *same* instance across **parallel** tasks (parallel project execution and the worker API). It does **not** wrap your methods in a lock. So `var count = 0; count++` is a race — use `AtomicInteger`. A shared cache must be a `ConcurrentHashMap` or guarded by synchronization. - **Idempotent, side-effect-aware methods.** Since calls can interleave, design methods to be safe under concurrency. - **Resource limits.** If the wrapped resource (e.g. an external server connection pool) can't take unlimited concurrent callers, set `maxParallelUsages`. ## maxParallelUsages ```kotlin gradle.sharedServices.registerIfAbsent("http", HttpService::class) { maxParallelUsages.set(4) // at most 4 tasks use it concurrently } ``` Gradle treats the service like a constrained resource and won't let more than N using-tasks run at once. Tasks declare usage via `usesService` / `@ServiceReference`, which is what lets Gradle enforce this. ## Putting it together ```kotlin abstract class StatsService : BuildService<BuildServiceParameters.None>, AutoCloseable { private val counts = java.util.concurrent.ConcurrentHashMap<String, Int>() fun record(key: String) = counts.merge(key, 1, Int::plus) override fun close() { println("totals: $counts") } } ``` One instance, thread-safe internals, deterministic dump on close — the safe replacement for a static `MutableMap` that the configuration cache would mishandle.

  • Does Gradle synchronize calls into the service for you?
    No. It guarantees a single shared instance but not mutual exclusion. Parallel tasks can call concurrently, so you must make internal state thread-safe yourself.
  • When does close() run and how many times?
    Once, after the last task using the service finishes, only if the service implements AutoCloseable. It's the place to release resources or emit final aggregates.

Gradle gives you one shared whiteboard and erases it once at the end, but it won't stop two people writing on it at the same time — you bring the marker-passing rule (the lock).

saying these in an interview costs you the question

  • Assuming Gradle locks service methods, then using a plain non-thread-safe field.
  • Releasing resources in a task action instead of close(), leading to early teardown or leaks.
  • Believing a new instance is created per task or per subproject.

context