skip to content

Which injected services replace common Project APIs for configuration-cache compatibility, and how do you obtain them in a task or plugin?

level: seniorimportance: should knowfreq 40%

answer

  1. ProjectLayout=paths, ExecOperations=exec, FileSystemOperations=copy/delete
  2. ObjectFactory=properties, ProviderFactory=env/system/of
  3. @get:Inject abstract val on abstract task/plugin
  4. services are stateless handles, cache-safe
  5. constructor @Inject also works

basics

~10 s

Inject ProjectLayout (replaces project.file/buildDir), ExecOperations (replaces project.exec), FileSystemOperations (replaces project.copy/delete), ObjectFactory, and ProviderFactory. Obtain them via constructor @Inject (or an abstract @get:Inject getter) — never reference Project.

solid answer

~40 s

The configuration cache forbids the live `Project` at execution time, so Gradle exposes a set of **injected services** that provide the same capabilities in a cache-safe, serializable-friendly way. The main ones: `ProjectLayout` for paths (`layout.buildDirectory`, `layout.projectDirectory`) replacing `project.buildDir`/`project.file`; `ExecOperations` for running processes, replacing `project.exec`/`javaexec`; `FileSystemOperations` for `copy`/`delete`/`sync`, replacing the Project equivalents; `ObjectFactory` for creating managed types and properties (`objects.property(...)`, `objects.fileProperty()`); and `ProviderFactory` for `environmentVariable`/`systemProperty`/`gradleProperty`/`of`. In a task or plugin you obtain them via `@Inject` — typically an `abstract @get:Inject val` on an abstract `DefaultTask`/plugin class, or a constructor parameter. Because they are services (not the Project), they don't pin build state into the serialized graph, so you can safely use them inside `@TaskAction` bodies and `ValueSource`/`BuildService` implementations.

code

kotlin · 20 lines
kotlin
abstract class SyncDocs : DefaultTask() {
    @get:Inject abstract val fs: FileSystemOperations
    @get:Inject abstract val layout: ProjectLayout

    @get:InputDirectory abstract val src: DirectoryProperty
    @get:OutputDirectory abstract val dest: DirectoryProperty

    @TaskAction
    fun run() {
        fs.sync {              // replaces project.sync
            from(src)
            into(dest)
        }
    }
}

tasks.register<SyncDocs>("syncDocs") {
    src.set(layout.projectDirectory.dir("docs"))
    dest.set(layout.buildDirectory.dir("docs"))
}

go deeper

for a junior

Name at least one service (e.g. ProjectLayout) and that you get it via @Inject.

for a middle

Map the common Project methods to their injected-service replacements and inject them correctly.

for a senior

Apply injection in tasks, ValueSources, and BuildServices; explain why services serialize but Project doesn't.

for a principal

Standardize injected-service usage in shared plugins so all custom tasks are cache-compatible by default.

## Why injected services exist The Project is a one-stop shop: it can create files, run processes, copy trees, make properties, read env vars. All of that is convenient at configuration time but unusable at execution time under the configuration cache (the Project is live and non-serializable). Gradle's answer is to split those capabilities into **fine-grained services** that you request explicitly and that are cache-safe. ## The service-to-Project mapping | Need | Injected service | Replaces | |------|------------------|----------| | Paths / build dir | `ProjectLayout` | `project.buildDir`, `project.file`, `project.projectDir` | | Run a process | `ExecOperations` | `project.exec`, `project.javaexec` | | Copy / delete / sync files | `FileSystemOperations` | `project.copy`, `project.delete`, `project.sync` | | Create managed objects/properties | `ObjectFactory` | `project.objects` usage at execution time | | Tracked external reads / lazy values | `ProviderFactory` | `project.providers`, env/system/property reads | | Resolve files lazily | `ProjectLayout` + `Provider` | `project.files(...)` | ## How to inject The idiomatic form on an **abstract** task or plugin: ```kotlin abstract class PackageTask : DefaultTask() { @get:Inject abstract val fs: FileSystemOperations @get:Inject abstract val layout: ProjectLayout @get:Inject abstract val execOps: ExecOperations @get:OutputDirectory abstract val outDir: DirectoryProperty @TaskAction fun run() { fs.copy { from(layout.projectDirectory.dir("src")) into(outDir) } } } ``` Gradle's dependency injection fills these in. You can also use constructor injection: ```kotlin open class Foo @Inject constructor( private val execOps: ExecOperations, private val layout: ProjectLayout, ) : DefaultTask() ``` Inside a `ValueSource` or `BuildService`, the same `@get:Inject abstract val` pattern provides services without touching the Project. ## Why this is cache-safe These services are **stateless handles** Gradle re-provides on each build; they don't capture the project graph the way a `Project` reference would, so serializing a task that holds them is fine. By contrast, holding `Project` would try to serialize the entire live model. ## Practical rule Whenever you reach for `project.X` inside a task action, ask: *is there an injected service for X?* For files, processes, copies, objects, and providers, the answer is yes — use it. Reserve direct Project access for the configuration phase only.

  • How do you inject a service into a ValueSource or BuildService, which have no constructor you control conventionally?
    Declare an abstract `@get:Inject val` for the service (e.g. `@get:Inject abstract val exec: ExecOperations`) on the abstract implementation class. Gradle's object factory supplies it, exactly as in tasks — no Project needed.
  • Why is holding an `ExecOperations` reference in a serialized task fine, but holding `Project` is not?
    ExecOperations is a small stateless service handle Gradle re-supplies per build; it doesn't drag in the build model. A Project reference would require serializing the entire live, mutable project graph, which the cache cannot do.

saying these in an interview costs you the question

  • Saying you can't run processes or copy files under the config cache at all — you can, via injected services.
  • Trying to serialize/store the Project to work around it.
  • Confusing `ObjectFactory` (creating managed types) with `ProviderFactory` (reading tracked values).

context