skip to content

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

level: seniorimportance: should knowfreq 28%

answer

  1. lazy create on first declared use
  2. single shared instance per build
  3. close() once at build end
  4. service body must be thread-safe
  5. rebuild resource from params (cc hit)

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.

solid answer

~50 s

Lifecycle: at registration you only get a `Provider`; the single shared instance is **constructed lazily** the first time a task that declares it executes. From then on every consumer shares that one instance for the duration of the build. If the service implements **`AutoCloseable`**, Gradle invokes `close()` exactly once when the build finishes — the place to flush caches, stop a server, or close connections you opened in the constructor or on demand. Because parallel tasks (and worker actions inside a task) can call the service concurrently, **the service body must be thread-safe**: use `AtomicInteger`/concurrent collections, synchronize mutable sections, or make the resource itself concurrency-safe. `maxParallelUsages` limits how many *tasks* hold it, but never substitutes for internal synchronization. Keep heavy startup in the constructor (or a lazily-initialized field) so the cost is paid once and amortized across all users.

code

kotlin · 8 lines
kotlin
abstract class ConnectionPool : BuildService<BuildServiceParameters.None>, AutoCloseable {
    private val pool = createPool()            // built once, lazily
    fun withConnection(block: (Conn) -> Unit) {
        val c = pool.borrow()                  // pool is itself thread-safe
        try { block(c) } finally { pool.release(c) }
    }
    override fun close() = pool.shutdown()      // called once at build end
}

go deeper

for a junior

Knows the service is shared and that AutoCloseable.close() cleans it up.

for a middle

Describes lazy creation, single instance, and that methods can be called in parallel.

for a senior

Details the full lifecycle, thread-safety requirement independent of maxParallelUsages, and config-cache reconstruction from params.

for a principal

Defines org standards for resource-holding services: deterministic close(), thread-safe internals, and config-cache-safe param-driven construction.

## Phase 1 — Registration (configuration time) ```kotlin val cache = gradle.sharedServices.registerIfAbsent("buildCache", CacheService::class) { parameters.dir.set(layout.buildDirectory.dir("svc-cache")) } ``` Nothing is instantiated. You receive a `Provider<CacheService>`. Registration is idempotent by name. ## Phase 2 — Lazy creation (first use, execution time) The instance is created the **first time** a declaring task runs and the provider is realized. Gradle constructs it with a no-arg constructor, injecting `parameters` (and any `@Inject` services). Expensive setup placed in the constructor runs **once**. ```kotlin abstract class CacheService : BuildService<CacheService.Params>, AutoCloseable { interface Params : BuildServiceParameters { val dir: DirectoryProperty } private val store = ConcurrentHashMap<String, ByteArray>() // thread-safe private val file = parameters.dir.get().asFile.also { it.mkdirs() } fun get(key: String): ByteArray? = store[key] fun put(key: String, v: ByteArray) { store[key] = v } override fun close() { /* flush store to file */ } } ``` ## Phase 3 — Shared concurrent use Every task that declares the service (`usesService`/`@ServiceReference`) gets the **same instance**. With parallel execution, multiple tasks — and multiple worker items within one task — may call methods at the same time. **Gradle does not serialize these calls for you.** Hence the service must be internally thread-safe: prefer immutable state, atomics, `ConcurrentHashMap`, or explicit locks around critical sections. `maxParallelUsages(n)` caps concurrent *tasks*, which can reduce contention, but with n>1 (or worker fan-out inside one task) concurrent calls still occur. ## Phase 4 — Shutdown (build end) If the service is `AutoCloseable`, Gradle calls `close()` **once** at the end of the build, regardless of pass/fail, after all tasks complete. This is the deterministic place to release resources. Don't rely on JVM shutdown hooks or finalizers — `close()` is the contract. ## Configuration cache nuance On a configuration-cache **hit**, configuration code doesn't re-run, but the service registration (its params snapshot) is restored, and the instance is still created lazily at execution. So your service must reconstruct any live resource from its serialized **params**, never from captured configuration-time objects. ## Checklist - Heavy init in constructor/lazy field → paid once. - Thread-safe state → required, independent of maxParallelUsages. - Implement `AutoCloseable` for deterministic cleanup. - Reconstruct live resources from serializable params (config-cache safe).

  • If maxParallelUsages is 4, do you still need internal synchronization?
    Yes — up to 4 tasks (plus any worker fan-out within them) can call the service concurrently, so its mutable state must be thread-safe regardless of the cap.
  • On a configuration-cache hit, how does the service get its live resource back?
    Configuration code doesn't re-run; the service is recreated lazily at execution and must rebuild the resource from its serialized params, not from captured config-time objects.

Like a shared coffee machine in an office: switched on the first time someone wants coffee (lazy), used by everyone at once so it must handle concurrent users (thread-safe), and switched off once at end of day (close()).

saying these in an interview costs you the question

  • Relying on JVM shutdown hooks/finalizers instead of AutoCloseable.close() for cleanup.
  • Assuming maxParallelUsages removes the need for thread-safe service code.
  • Capturing configuration-time objects in the service instead of rebuilding from params (breaks config cache).

context