What does the @ServiceReference annotation do on a Gradle task, and why is it preferable to manually wiring a build service?
answer
- abstract Property<MyService> on task
- auto-wire registered build service
- no manual usesService()
- optional name match -> may be undefined
- config-cache friendly
basics
~10 s@ServiceReference marks an abstract Property<MyService> on a task so Gradle auto-injects a registered shared build service and automatically tracks it as a task dependency, so you don't call usesService() yourself.
solid answer
~40 sA shared **BuildService** is a long-lived object you register once and reuse across tasks (e.g. a connection pool or counter). Normally a task declares an abstract `Property<MyService>`, you assign it the `Provider` from registration, and you call `task.usesService(provider)` so Gradle knows the task uses it (important for `--max-workers` constraints). `@ServiceReference` collapses that: you annotate the abstract property, optionally with a service name, and Gradle **auto-wires** the matching registered service by type (or name) and **implicitly marks the task as using it**. This removes boilerplate and the easy-to-forget `usesService()` call. If you supply a name and no service with that name is registered, the property stays undefined — so the task must handle absence (e.g. via `@Optional`). It's the configuration-cache-friendly, declarative way to consume a build service from a task.
code
kotlin · 7 linesabstract class GreetTask : DefaultTask() {
@get:ServiceReference("greeter")
abstract val greeter: Property<GreeterService>
@TaskAction
fun greet() = logger.lifecycle(greeter.get().hello())
}go deeper
Know it auto-injects a shared build service into an abstract Property on a task so you don't wire it manually.
Explain that it replaces both the provider assignment and usesService(), and that a named reference may resolve to nothing.
Discuss the parallelism implication (maxParallelUsages / usesService) and configuration-cache friendliness of the lazy property.
Frame it as the declarative consumption surface for cross-task shared state in a plugin's API, and how naming conventions keep services discoverable across a multi-plugin build.
## What a build service is A **shared build service** is an object whose lifecycle Gradle manages for the whole build. You implement `BuildService<Params>` (or `BuildService<BuildServiceParameters.None>`), register it once, and many tasks can share the same instance. Typical uses: a shared counter, an in-memory cache, a pool of expensive connections, or a wrapper around an external process. ## The manual wiring it replaces Without `@ServiceReference`, consuming a service from a task takes three steps: 1. Register the service, getting a `Provider<MyService>`. 2. Declare an abstract `@get:Internal abstract val server: Property<MyService>` on the task. 3. Assign the provider **and** call `task.usesService(provider)`. That last call is the part people forget. `usesService()` tells Gradle the task *uses* this service, which matters because a service can declare `getMaxParallelUsages()` — Gradle then constrains how many tasks using it run concurrently against `--max-workers`. Forget `usesService()` and that throttling silently doesn't apply. ## What @ServiceReference does `@ServiceReference` is a property annotation. You put it on the abstract `Property<MyService>`: - Gradle **auto-injects** the registered service of the matching type when the task runs. - It **implicitly registers the usage** (no manual `usesService()`), so parallelism limits and dependency ordering apply. - You can pass a **name**: `@ServiceReference("webServer")`. Then Gradle matches the registration by that name. If nothing with that name is registered, the property is simply left without a value. Because matching can fail (named service not registered), treat the property as possibly-absent: combine with `@Optional` or guard with `.isPresent`. ## Configuration cache fit The property is a lazy `Provider`/`Property`, so the service instance is resolved at execution time, not stored eagerly — which is exactly what the configuration cache wants. ```kotlin abstract class CountTask : DefaultTask() { @get:ServiceReference("counter") abstract val counter: Property<CounterService> @TaskAction fun run() { val n = counter.get().increment() logger.lifecycle("count = $n") } } ``` Here the `counter` service is wired by name and the task is automatically marked as using it.
- What does @ServiceReference save you from calling manually?The explicit Task.usesService(provider) call — it implicitly registers the task's usage of the service so parallelism limits and ordering apply.
- What happens if you give @ServiceReference a name that no registered service matches?The property is left without a value. The task must tolerate absence (e.g. pair with @Optional or check isPresent); accessing .get() would fail.
saying these in an interview costs you the question
- Claiming @ServiceReference registers the service itself — it only consumes an already-registered service.
- Saying you still must call usesService() alongside @ServiceReference — that's the boilerplate it removes.