skip to content

How does Gradle resolve which registered build service to inject for a @ServiceReference, with and without an explicit name?

level: seniorimportance: should knowfreq 30%

answer

  1. no name -> match by type
  2. name -> match registration name
  3. missing named match = unset, not error
  4. pair with @Optional
  5. lazy resolve at execution

basics

~10 s

Without a name, Gradle injects the single registered service whose type matches the property's type. With a name, it matches the registration name; if none matches, the property stays undefined rather than failing.

solid answer

~50 s

When you write `@ServiceReference` with no argument, Gradle looks for a registered build service whose implementation type is compatible with the property's declared type and injects it. If exactly one matches, it's wired; ambiguity or absence leaves the property unset. When you pass a **name** — `@ServiceReference("webServer")` — Gradle matches the registration name you gave to `registerIfAbsent("webServer", ...)`. A named reference that matches nothing does **not** error at configuration time; the property simply has no value, so the task should pair it with `@Optional` or guard `isPresent`. Naming is the robust choice when several services of the same type exist, or when a plugin wants to expose a service under a stable contract name that other plugins can target. The resolution is lazy: the actual `BuildService` instance is materialized when the property is queried at execution, which keeps it configuration-cache compatible.

code

kotlin · 9 lines
kotlin
// registration supplies the name used by @ServiceReference("counter")
val provider = gradle.sharedServices.registerIfAbsent(
    "counter", CounterService::class) { parameters.start.set(0) }

abstract class UseTask : DefaultTask() {
    @get:ServiceReference("counter")
    @get:Optional
    abstract val counter: Property<CounterService>
}

go deeper

for a junior

Know there are two forms: by type (no arg) and by name (string arg).

for a middle

Explain that named matching is by the registerIfAbsent name and that a missing match leaves the property unset.

for a senior

Discuss ambiguity with multiple same-type services and the @Optional defensive pattern, plus lazy execution-time resolution.

for a principal

Treat service names as a cross-plugin contract surface and reason about governance of those names across a large multi-module build.

## Registration recap You register a build service through the build's service registry: ```kotlin val counter = gradle.sharedServices.registerIfAbsent("counter", CounterService::class) { parameters.start.set(0) } ``` The first argument, `"counter"`, is the **service name**. `registerIfAbsent` returns a `Provider<CounterService>` and is idempotent: a second call with the same name returns the existing registration. ## Type-based resolution (no name) `@get:ServiceReference abstract val counter: Property<CounterService>` with no name asks Gradle to find a registered service assignable to `CounterService`. This is convenient but fragile: if two services share that type (or none do), resolution can't pick one deterministically and the property is left unset. ## Name-based resolution `@get:ServiceReference("counter")` binds to the registration whose name is `"counter"`. This is deterministic and the recommended form when: - multiple services implement the same type, or - you want a stable, documented contract name that consumers across modules/plugins can rely on. ## Missing matches are not errors A crucial gotcha: a named reference that resolves to nothing does **not** fail the build at configuration. The property is simply value-less. So author tasks defensively: ```kotlin @get:ServiceReference("counter") @get:Optional abstract val counter: Property<CounterService> @TaskAction fun run() { if (counter.isPresent) counter.get().increment() } ``` ## Laziness & configuration cache The injected value is a `Provider`/`Property`, resolved on `.get()` at execution time. Gradle does not serialize the live service into the configuration cache; it re-resolves it, which is why build services are a supported way to hold mutable state under the configuration cache (where you cannot reach into the `Project` at execution time).

  • Why prefer a named @ServiceReference when several services share a type?
    Type-only matching can't deterministically choose among multiple services of the same type, so resolution becomes ambiguous; a name pins the exact registration.
  • Does a missing named service fail the build?
    No — at configuration the property is simply left unset. It only fails if the task calls .get() on the absent value, so you guard with @Optional / isPresent.
  • Why is @ServiceReference safe under the configuration cache?
    It exposes the service as a lazy Provider resolved at execution, so the live instance isn't serialized; Gradle re-resolves the registered service per build.

saying these in an interview costs you the question

  • Saying a missing named service fails at configuration time.
  • Claiming type-based resolution always picks the 'first' matching service — ambiguity leaves it unresolved, not first-wins.

context