skip to content

How do you make a task depend on a BuildService so it's tracked as a dependency, and what does @ServiceReference add over manually wiring usesService?

level: seniorimportance: should knowfreq 38%

answer

  1. Property<MyService> on the task
  2. usesService(provider) registers the dependency
  3. @ServiceReference(name) = auto-use + auto-inject
  4. not an @Input; use @Internal/@ServiceReference
  5. reference tracked → config-cache safe

basics

~20 s

Expose an abstract Property<MyService> on the task and either set it from the provider plus call usesService(provider), or annotate it with @ServiceReference("name") so Gradle auto-wires and registers the usage. @ServiceReference removes the manual wiring boilerplate.

solid answer

~40 s

A task references a build service through a managed **`Property<MyService>`**. The older explicit way: set the property from the registration provider and call `task.usesService(provider)` so Gradle records the dependency — this guarantees the service is created before the task runs and respects `maxParallelUsages`. The modern way is **`@ServiceReference`** on the property: `@get:ServiceReference("counter") abstract val counter: Property<CounterService>`. Gradle then (a) automatically marks the service as **used** by the task (no manual `usesService`), and (b) if a service with that name is registered, **injects it without you setting the property**. This keeps the task config-cache safe because the reference, not the live object, is what's tracked. Without registering usage somehow, the service could be instantiated lazily off the build thread or torn down at the wrong time.

code

kotlin · 11 lines
kotlin
abstract class ReportTask : DefaultTask() {
    @get:ServiceReference("counter")
    abstract val counter: Property<CounterService>

    @TaskAction
    fun run() = println(counter.get().next())
}

// No usesService(), no .set() needed: Gradle injects the
// service registered under the name "counter".
tasks.register<ReportTask>("report")

go deeper

for a junior

Know that a task exposes a Property<MyService> and must declare it uses the service.

for a middle

Show both usesService wiring and @ServiceReference, and why the property is @Internal not @Input.

for a senior

Explain lifecycle/throttling/config-cache reasons the usage must be declared, and the auto-injection semantics of @ServiceReference.

for a principal

Standardize on @ServiceReference for libraries to make correct usage the default and avoid forgotten usesService calls.

## Why a task must *declare* its use of a service Merely calling `provider.get()` inside a `doLast {}` is not enough. Gradle needs to know a task **uses** a service so it can: - ensure the single instance is created **before** the task executes, - enforce `maxParallelUsages` (throttling), - order service teardown (`AutoCloseable.close()`) after the last using task, - record the dependency in the **configuration cache** so reuse is correct. ## The explicit wiring (pre-`@ServiceReference`) ```kotlin abstract class ReportTask : DefaultTask() { @get:Internal abstract val counter: Property<CounterService> @TaskAction fun run() { println(counter.get().next()) } } val counter = gradle.sharedServices.registerIfAbsent("counter", CounterService::class) {} tasks.register<ReportTask>("report") { this.counter.set(counter) // wire the provider usesService(counter) // register the usage } ``` Note `@Internal` (or `@ServiceReference`) — the service property is **not** a task input/output, so it must not be annotated with `@Input`. ## The modern wiring with `@ServiceReference` ```kotlin abstract class ReportTask : DefaultTask() { @get:ServiceReference("counter") abstract val counter: Property<CounterService> @TaskAction fun run() { println(counter.get().next()) } } ``` `@ServiceReference("counter")` does two things: 1. **Auto-registers usage** — equivalent to calling `usesService`, so you don't write it by hand and can't forget it. 2. **Auto-injects by name** — if a service named `counter` is registered anywhere in the build, Gradle injects it; you don't even need to call `.set(...)`. If no such service is registered and the property is left unset, the task fails clearly. You may pass `@ServiceReference` with no name to require explicit wiring while still auto-registering usage, or with a name to opt into auto-injection. ## Config-cache angle In all cases the task stores a **`Property<MyService>`** whose value is the service *provider/reference*, which the configuration cache serializes as a reference to the registration — not a snapshot of the service's mutable state. That's precisely what makes the service the safe home for shared state when configuration is cached and skipped on later runs. ## Summary `@ServiceReference` is sugar that (1) eliminates the manual `usesService` call and (2) optionally auto-injects the named service, reducing wiring errors. Functionally the dependency tracking is the same — it's about ergonomics and correctness-by-default.

  • What annotation should the service Property carry, and why not @Input?
    @ServiceReference or @Internal. The service is shared state, not a task input/output; treating it as @Input would try to snapshot it for up-to-date checks, which is wrong and breaks serialization.
  • What happens if you call provider.get() in a task action but never declare usesService or @ServiceReference?
    Gradle hasn't recorded the dependency, so lifecycle and maxParallelUsages aren't guaranteed and the config cache may not track it correctly. Modern Gradle flags undeclared service usage; you should declare it.

saying these in an interview costs you the question

  • Annotating the service property with @Input.
  • Calling the service from a configuration-time block instead of the task action.
  • Assuming @ServiceReference changes dependency semantics — it only removes boilerplate and adds auto-injection.

context