What is constructor service injection in Gradle, and why do tasks and plugins obtain services like ObjectFactory or ProviderFactory via @Inject instead of through the project object?
answer
- @Inject abstract getter
- Gradle instantiator fills it
- Project unavailable at execution under config cache
- ObjectFactory/ProviderFactory/ExecOperations/FileSystemOperations
- decouple from project.*
basics
~20 sGradle injects built-in services (like ObjectFactory) into a task or plugin through an @Inject-annotated constructor or abstract getter. You ask Gradle for the service instead of reaching through the project, which keeps the code decoupled and configuration-cache compatible.
solid answer
~40 sGradle owns a set of built-in services — `ObjectFactory`, `ProviderFactory`, `ExecOperations`, `FileSystemOperations`, `ArchiveOperations`, `ProjectLayout`, `WorkerExecutor`. Rather than create them yourself, you declare them and Gradle injects instances. In a task or plugin you add an `@Inject`-annotated abstract getter (or constructor parameter); Gradle's instantiator fills it in when it creates the object. The key motivation is the configuration cache: at execution time the `Project` instance is NOT available (it isn't serializable), so calling `project.exec`, `project.copy`, or `project.objects` from a task action is forbidden. Injected services ARE available at execution time because Gradle tracks and re-provides them. So injection both decouples code from `Project` and is the supported way to do exec/copy/file work in a config-cache-safe task.
code
kotlin · 11 linesabstract class GenerateTask : DefaultTask() {
@get:Inject abstract val objects: ObjectFactory
@get:Inject abstract val execOps: ExecOperations
@get:OutputDirectory abstract val outputDir: DirectoryProperty
@TaskAction
fun run() {
execOps.exec { commandLine("git", "rev-parse", "HEAD") }
}
}go deeper
Know that Gradle gives you helper objects via @Inject and you don't create them yourself.
Name the main services, show an abstract @Inject getter, and explain the decoupling-from-project motivation.
Tie injection directly to configuration-cache compatibility — why Project is unavailable at execution and which services replace project.exec/copy/objects.
Frame injected services as Gradle's DI/service-locator architecture; set conventions so all team plugins are written cache-safe from day one and audited in CI.
## What "injected services" means Gradle is built on a service-locator/dependency-injection runtime. It hosts a registry of **built-in services** — small objects that perform a focused job. The common ones you'll inject: - `ObjectFactory` — creates managed objects, `Property<T>`, `ListProperty`, `SetProperty`, `MapProperty`, `DomainObjectSet`, and instances of `@Inject`-using types. - `ProviderFactory` — creates `Provider`s, reads environment variables / system properties / Gradle properties lazily (`providers.environmentVariable("X")`), and runs `providers.exec { }` at configuration time in a cache-safe way. - `ExecOperations` — runs external processes (`exec`, `javaexec`) at execution time. - `FileSystemOperations` — does `copy`, `sync`, `delete` at execution time. - `ArchiveOperations` — creates `zipTree` / `tarTree` file trees lazily. - `ProjectLayout` — `buildDirectory`, `projectDirectory` as providers. - `WorkerExecutor` — submits work to the Worker API. ## How you inject them In a **task** or **plugin** (any type Gradle instantiates), add an abstract getter annotated `@Inject`, or a constructor parameter annotated `@Inject`: ```kotlin abstract class MyTask : DefaultTask() { @get:Inject abstract val execOps: ExecOperations @get:Inject abstract val fsOps: FileSystemOperations @get:Inject abstract val objects: ObjectFactory } ``` Gradle's instantiator sees the `@Inject` annotation and supplies the service when it constructs the object. Abstract getters are preferred because Gradle generates the backing implementation; you never `new` the service. ## Why not just use `project.*`? Under the **configuration cache**, Gradle serializes the task graph after the configuration phase and reuses it. The `Project` object is intentionally NOT serializable and is unavailable during task execution. So `project.exec { }`, `project.copy { }`, `project.objects`, `project.layout` inside a `@TaskAction` are flagged as config-cache problems. Injected services solve this: Gradle knows how to provide them at execution time, so a task holding `ExecOperations` can run a process even with the cache enabled. ## Where injection works Injection works in types Gradle instantiates: tasks, `Plugin<T>`, `@Inject`-aware extensions, `ValueSource`, `BuildService`, and worker `WorkAction` parameters. In a plain `build.gradle.kts` **script** you can't use `@Inject` on a local — there you use `gradle.serviceOf<T>()` or the script's built-in accessors (`objects`, `providers`, `layout`).
- Why does Gradle prefer abstract getters over a constructor parameter for injection?Abstract getters let Gradle generate the backing field and implementation, keep the class lighter, and play well with managed-property generation; constructor injection still works but you write more boilerplate and can't mix it as cleanly with generated properties.
- Can you inject services into a plain build script local variable with @Inject?No — @Inject only works on types Gradle instantiates (tasks, plugins, services). In a script you use the built-in accessors (objects, providers, layout) or gradle.serviceOf<T>().
saying these in an interview costs you the question
- Saying you should `new` an ObjectFactory or ProviderFactory yourself — you never construct services.
- Claiming project.exec/project.copy are fine inside a @TaskAction under the configuration cache.
- Confusing @Inject (Gradle's own) with javax.inject from a DI framework like Spring/Dagger at runtime.