skip to content

How do you make an ad-hoc task cacheable using the runtime API, and what role do inputs/outputs play in that?

level: middleimportance: should knowfreq 45%

answer

  1. cacheIf { true } opts ad-hoc tasks in
  2. @CacheableTask is for typed tasks
  3. doNotCacheIf vetoes
  4. inputs=key, outputs=payload
  5. normalize for relocatability

basics

~20 s

Declare all real inputs and outputs, then opt in with outputs.cacheIf { true } (and optionally outputs.doNotCacheIf for exclusions). The declared inputs form the cache key; the declared outputs are what gets stored and restored.

solid answer

~40 s

An ad-hoc task isn't cacheable by default — only typed tasks marked `@CacheableTask` are. To make an ad-hoc one cacheable you call `outputs.cacheIf { <predicate> }` on it; returning `true` opts that task into the build cache. The prerequisite is **complete, correct declarations**: `inputs.*` build the cache key and `outputs.*` are exactly what Gradle packs into and restores from the cache entry. If outputs are under-declared, a cache hit restores an incomplete result; if inputs include volatile values, you get false misses. You can also call `outputs.doNotCacheIf("reason") { <predicate> }` to skip caching in specific conditions (e.g. when output is empty or non-relocatable). Caching only helps if the inputs are normalized so identical logical work hashes identically across machines — otherwise the remote cache never hits.

code

kotlin · 7 lines
kotlin
tasks.register("pack") {
    inputs.dir("src/data").withPathSensitivity(PathSensitivity.RELATIVE)
    outputs.file(layout.buildDirectory.file("data.bin"))
    outputs.cacheIf { true }
    outputs.doNotCacheIf("empty") { fileTree("src/data").isEmpty }
    doLast { /* pack */ }
}

go deeper

for a junior

Know that caching is opt-in and ad-hoc tasks use outputs.cacheIf { true }.

for a middle

Explain inputs-as-key vs outputs-as-payload and why declarations gate cacheability; mention doNotCacheIf.

for a senior

Discuss relocatability/normalization for cross-machine hits and when caching is net-negative.

for a principal

Reason about remote-cache strategy, hit-rate measurement, and policy for which tasks teams should cache.

## Cacheability is opt-in Gradle's build cache stores task outputs keyed by the task's input fingerprint, so a later build (same machine or a teammate / CI via a remote cache) can **restore** outputs instead of executing. But caching is **opt-in per task** for safety — a wrongly-cached task corrupts builds. - **Typed tasks**: annotate the class with `@CacheableTask`. - **Ad-hoc tasks**: there's no annotation, so you use the runtime API: `outputs.cacheIf { true }`. ## The runtime caching API - `outputs.cacheIf("reason") { predicate }` — opt in when the predicate is true. Multiple `cacheIf` are AND-ed (all must be true). - `outputs.doNotCacheIf("reason") { predicate }` — veto caching when true (e.g. output dir is empty, or contains absolute paths that can't relocate). Any `doNotCacheIf` returning true wins. ## Why declarations are the foundation The cache literally cannot work without good declarations: - **Inputs → the key.** Every `inputs.file/dir/property` contributes to the hash that identifies the cache entry. Miss one and two genuinely-different builds collide on the same key (wrong restore). Include a volatile one and identical builds get different keys (no reuse). - **Outputs → the payload.** Gradle packs the declared `outputs.file/dir` into the cache entry and restores exactly those on a hit. An undeclared produced file is simply absent after a restore. ## Relocatability For the cache to be shared across machines (different checkout paths, different build dirs), the inputs must be **normalized**: path sensitivity set appropriately so absolute paths don't leak into the key, classpath order normalized, etc. Without that, a remote cache populated by CI never hits on a developer machine. ## Example ```kotlin tasks.register("renderTemplates") { inputs.dir("src/templates").withPathSensitivity(PathSensitivity.RELATIVE) inputs.property("locale", project.findProperty("locale") ?: "en") val outDir = layout.buildDirectory.dir("rendered") outputs.dir(outDir) outputs.cacheIf("templates render deterministically") { true } outputs.doNotCacheIf("no templates present") { fileTree("src/templates").isEmpty } doLast { /* render src/templates into build/rendered */ } } ``` ## Caveat Caching tiny/fast tasks can be slower than re-running them (pack/unpack/transfer overhead). Cache only tasks whose execution clearly costs more than a cache round-trip, and always verify with a build scan that hits actually occur.

  • Why isn't every task cached by default?
    Caching is unsafe for tasks with non-deterministic or non-relocatable outputs, or with hidden undeclared inputs. Opt-in (@CacheableTask or cacheIf) forces the author to assert the task is deterministic and fully declared before its results are reused.
  • When might caching a task actually hurt?
    For very cheap tasks, the overhead of computing the key, packing, transferring, and unpacking can exceed just re-executing — especially over a remote cache. Cache only tasks whose work clearly outweighs the round-trip.

saying these in an interview costs you the question

  • Saying outputs.cacheIf is the only thing needed, ignoring that complete input/output declarations are the prerequisite.
  • Believing @CacheableTask works on ad-hoc tasks (it's a class annotation for typed tasks).
  • Caching everything indiscriminately, including trivially cheap tasks.

context