What problems arise when you resolve input/output files eagerly at configuration time instead of using lazy providers, and how do you avoid them?
answer
- .get()/File at config time = bad
- loses producer => inference breaks
- work runs even if task skipped
- breaks configuration cache
- layout.buildDirectory + Providers
- resolve only inside doLast
basics
~10 sResolving files eagerly (.get(), new File(...)) at configuration time loses producer links (breaking dependency inference), does work even when the task won't run, and breaks the configuration cache. Use Providers and layout.buildDirectory instead.
solid answer
~40 sDeclaring inputs/outputs with eagerly-resolved paths — `File("build/out.txt")`, `provider.get()`, `outputs.files.singleFile` at configuration time — causes three problems. First, it severs the producer reference carried by a `Provider`, so Gradle can no longer infer the task dependency (you get implicit-dependency errors or wrong ordering). Second, it forces computation during configuration even for tasks that won't execute, hurting configuration time and breaking incrementality. Third, it's incompatible with the **configuration cache**, which requires task wiring to be serializable and lazy. The fix is to declare inputs/outputs with lazy, provider-based values: use `layout.buildDirectory.file(...)`/`.dir(...)` for output locations, pass `Provider`/`TaskProvider`/`FileCollection` into `inputs.file(s)`, and never call `.get()` at configuration time. Resolution then happens at execution, ordering is inferred, and the build stays cache-compatible.
code
kotlin · 5 linestasks.register("produce") {
val out = layout.buildDirectory.file("gen/data.txt")
outputs.file(out) // lazy provider, carries producer
doLast { out.get().asFile.writeText("hi") } // resolve only here
}go deeper
Know you should declare output paths via layout.buildDirectory and not hardcode 'build/...'.
Explain that resolving files eagerly can break dependency inference and do needless configuration work.
Tie eager resolution to lost producer links, configuration-time cost, and configuration-cache failures; give the lazy provider recipe.
Mandate lazy provider-based wiring across modules so configuration cache and incremental builds hold at scale.
## Eager vs lazy in input/output declarations Gradle has two ways to refer to files when declaring inputs/outputs: - **Eager**: a concrete `File`, a hardcoded path string resolved immediately, or a provider you `.get()` during configuration. - **Lazy**: a `Provider<RegularFile>`/`Provider<Directory>` (e.g. from `layout.buildDirectory.file(...)`), a `TaskProvider`, or a `FileCollection` — resolved only when needed. ## Three concrete problems with eager resolution ### 1. Lost producer link ⇒ broken dependency inference A lazy output provider carries 'which task produced me'. Resolving it to a plain `File` discards that, so a downstream `inputs.file(file)` can't infer the producer. With the configuration cache or property validation enabled, Gradle errors: > Task ':consume' uses output of ':produce' without declaring a dependency. Without those guards you may get a flaky, order-dependent build. ### 2. Work at configuration time Gradle configures **every** task on the requested build, even ones that won't run. If your declaration calls `.get()` or computes paths/IO eagerly, that cost is paid on every invocation regardless. Lazy providers defer the work to the execution phase of the few tasks that actually run. ### 3. Configuration-cache incompatibility The configuration cache serializes the configured task graph. It requires that wiring be expressed through serializable providers, not captured live objects resolved at configuration time. Eagerly captured `File`s and `.get()` calls are common causes of configuration-cache failures and 'cannot serialize' problems. ## The lazy recipe ```kotlin val produce = tasks.register("produce") { val out = layout.buildDirectory.file("gen/data.txt") // Provider<RegularFile> outputs.file(out) doLast { out.get().asFile.writeText("hi") } // .get() ONLY inside the action } tasks.register("consume") { inputs.files(produce) // keeps producer link val dest = layout.buildDirectory.dir("out") outputs.dir(dest) doLast { /* read producer outputs, write into dest.get() */ } } ``` Key rules: - Build output paths from `layout.buildDirectory` (or `project.layout`), never hardcode `build/...`. - Pass providers/task references into `inputs`/`outputs`; call `.get()` only inside `doLast`/`doFirst`. - Prefer `tasks.register` (lazy) over `tasks.create` (eager) so configuration is deferred too. ## Payoff Lazy declarations give correct dependency inference, faster configuration, and configuration-cache compatibility — all from the same input/output API, just used without premature resolution.
- Why does the configuration cache specifically dislike eager file resolution?It serializes the configured task graph and requires wiring to be lazy, serializable providers. Capturing live File objects or calling .get() at configuration time produces unserializable state or stale captured values, causing configuration-cache failures.
- Where is it safe to call provider.get()?Inside task actions (doLast/doFirst) at execution time, when the value is genuinely needed and the task is actually running. Never during configuration of the task.
- How does tasks.register help versus tasks.create here?register is lazy — the task is only configured if it ends up in the graph, deferring its input/output wiring; create is eager and configures immediately, paying the cost on every build.
Eager resolution is like printing a delivery address before you know the order — wasted effort if it's cancelled, and you lose the link back to who placed it.
saying these in an interview costs you the question
- Hardcoding 'build/...' paths instead of layout.buildDirectory.
- Calling .get()/.singleFile during configuration.
- Assuming eager declarations are fine 'because it works locally' — it breaks inference and the configuration cache.