How does Gradle resolve which registered build service to inject for a @ServiceReference, with and without an explicit name?
answer
- no name -> match by type
- name -> match registration name
- missing named match = unset, not error
- pair with @Optional
- lazy resolve at execution
basics
~10 sWithout 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 sWhen 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// 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
Know there are two forms: by type (no arg) and by name (string arg).
Explain that named matching is by the registerIfAbsent name and that a missing match leaves the property unset.
Discuss ambiguity with multiple same-type services and the @Optional defensive pattern, plus lazy execution-time resolution.
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.