skip to content

Why must a task call usesService() (or use @ServiceReference) instead of just calling provider.get(), and what breaks if it doesn't?

level: seniorimportance: should knowfreq 30%

answer

  1. declared vs undeclared get()
  2. enables maxParallelUsages enforcement
  3. config-cache recognizes the reference
  4. @ServiceReference injects + declares
  5. usesService for imperative wiring

basics

~10 s

Declaring usage tells Gradle the task depends on the service so it manages lifecycle and concurrency. Without it, maxParallelUsages isn't enforced and the configuration cache may warn or fail.

solid answer

~40 s

`usesService(provider)` (and `@ServiceReference`, which does it implicitly) registers an explicit dependency from the task to the BuildService. This is what lets Gradle (1) **enforce `maxParallelUsages`** — only declared usages count toward the concurrency cap; (2) track the service as a recognized input so it cooperates with the **configuration cache** rather than tripping warnings about undeclared shared state; and (3) order lifecycle correctly. Merely calling `provider.get()` inside `doLast` retrieves the instance but leaves Gradle blind to the relationship: the parallelism limit silently won't apply, and the dependency is undeclared. `@ServiceReference("name")` on a `Property<MyService>` is the preferred declarative form because it both injects the service and declares the usage in one step, and can even auto-locate a service registered under that name.

code

kotlin · 10 lines
kotlin
abstract class IntegTest : DefaultTask() {
    @get:ServiceReference("webServer")
    abstract val server: Property<WebServer>

    @TaskAction
    fun run() {
        val port = server.get().port // declared usage: counted + lifecycle-managed
        // ... run tests against port
    }
}

go deeper

for a junior

Knows you should call usesService when a task uses a service.

for a middle

Can wire both usesService and @ServiceReference and explains they declare a dependency.

for a senior

Explains the concrete consequences of skipping declaration: no maxParallelUsages enforcement, config-cache warnings.

for a principal

Mandates declarative @ServiceReference in shared plugins and reviews for undeclared get() as a correctness smell.

## Two ways a task can touch a service 1. **Undeclared**: capture the `Provider` and call `provider.get()` somewhere in the task action. 2. **Declared**: tell Gradle about the dependency via `usesService(provider)` or a `@ServiceReference` property. They look similar but behave very differently. ## What declaration buys you ### Concurrency enforcement `maxParallelUsages` is only enforced for tasks that **declare** the service. An undeclared `get()` is invisible to the scheduler, so a service capped at, say, 1 can still be hit by several undeclared tasks simultaneously. Declaration is therefore mandatory for any concurrency guarantee. ### Configuration-cache cooperation Build services are the supported way to hold shared state across the configuration cache boundary. When a task **declares** the service, Gradle understands the reference and serializes/restores it correctly. Undeclared, hidden references to shared state are exactly what the configuration cache is designed to catch and warn about. ### Lifecycle / correctness Declaration lets Gradle reason about when the service is needed, keeping its lazy creation and `AutoCloseable` shutdown coherent with the task graph. ## The declarative form: @ServiceReference ```kotlin abstract class PingTask : DefaultTask() { @get:ServiceReference("http") abstract val http: Property<HttpClientService> @TaskAction fun run() = println(http.get().get("/ping")) } ``` `@ServiceReference("http")`: - **injects** the service registered under name `http`, - **declares** the usage (so `maxParallelUsages` and lifecycle apply), - and, if the property is left unset but a service with that name exists, **auto-wires** it. Without the name, the property must be explicitly connected to a provider, and Gradle still treats it as a declared usage. ## The imperative form: usesService ```kotlin val svc = gradle.sharedServices.registerIfAbsent("http", HttpClientService::class) { } tasks.register("ping") { usesService(svc) // declares the dependency doLast { svc.get().get("/ping") } } ``` Here you still call `get()`, but `usesService(svc)` makes the relationship explicit. ## What breaks without declaration - `maxParallelUsages` is **not** enforced — concurrency bugs on constrained resources. - The configuration cache may emit **problems/warnings** about undeclared shared state, and behavior can be unreliable across cache hits. - Build scans and dependency reasoning won't show the relationship. ## Rule of thumb Always declare. Prefer `@ServiceReference` for tasks (declarative, less boilerplate); use `usesService()` when you need to wire a service in an ad-hoc action or non-`DefaultTask` context.

  • What's the advantage of @ServiceReference("name") over usesService?
    It injects the service, declares the usage, and can auto-wire by name in one declarative property — less boilerplate and harder to forget the declaration than an imperative usesService call.
  • Can you still call provider.get() when you've declared usesService?
    Yes. usesService declares the dependency; you then call get() to obtain the instance. With @ServiceReference you call get() on the injected Property instead.

saying these in an interview costs you the question

  • Assuming provider.get() alone is enough — it doesn't register the dependency, so maxParallelUsages is ignored.
  • Thinking @ServiceReference and usesService are mutually exclusive concepts rather than two ways to declare the same dependency.

context