skip to content

Cacheable Tasks

What makes a task's outputs reusable: the @CacheableTask marker, fully declared and normalized inputs and outputs, and conditional cacheIf predicates. Interviewers ask why marking a task cacheable is not sufficient on its own.

on this pageshow

questions

5

What does it mean for a Gradle task to be cacheable, and what is the minimum required to make a task's outputs reusable from the build cache?

level: juniorimportance: must knowfreq 70%

answer

  1. key = hash of declared inputs
  2. @CacheableTask opt-in
  3. declare ALL inputs/outputs
  4. FROM-CACHE vs UP-TO-DATE
  5. undeclared input = silent wrong output

basics

~10 s

A cacheable task can store its outputs in the build cache and restore them later instead of re-running. It needs the @CacheableTask annotation and fully declared inputs and outputs.

solid answer

~40 s

A cacheable task is one whose outputs Gradle can store keyed by a hash of its inputs, then restore on a later build (or another machine) instead of executing. For a task to actually be cached, two things must hold: its type must opt in with `@CacheableTask` (or be enabled per-instance via `outputs.cacheIf {}`), and **all** inputs and outputs must be fully declared through annotations like `@Input`, `@InputFiles`, `@OutputFile`, etc. Gradle hashes those declared inputs into a cache key; if a matching entry exists, it copies the outputs from the cache and marks the task `FROM-CACHE`. Anything Gradle can't see — undeclared inputs, reads from absolute paths, system time — silently breaks correctness, so complete and accurate input/output declaration is the real prerequisite, not just the annotation.

code

kotlin · 12 lines
kotlin
@CacheableTask
abstract class Concat : DefaultTask() {
    @get:InputFiles
    @get:PathSensitive(PathSensitivity.RELATIVE)
    abstract val inputs: ConfigurableFileCollection

    @get:OutputFile
    abstract val output: RegularFileProperty

    @TaskAction
    fun run() { /* ... */ }
}

go deeper

for a junior

Know that @CacheableTask plus declared inputs/outputs lets Gradle reuse results, and that FROM-CACHE means the task didn't run.

for a middle

Explain the cache key as a hash of declared inputs and why undeclared inputs cause silent wrong restores; distinguish from up-to-date.

for a senior

Discuss relocatability/normalization and path sensitivity so entries are reusable across machines, and judge which tasks are worth making cacheable.

for a principal

Frame caching correctness as an org-wide invariant — full input declaration discipline, normalization policy, and the cost of a poisoned shared cache.

## What "cacheable" means Gradle's **build cache** is a key-value store mapping a **cache key** (a hash of everything that affects a task's output) to the task's **output files**. When a task runs, Gradle can store its outputs under that key. On a later build — even on a different machine or CI agent — if the same key is computed, Gradle restores the outputs and reports the task as `FROM-CACHE` instead of executing it. This differs from **up-to-date** (incremental) checking, which only skips work within the *same* build directory. A task is **cacheable** only if it opts in. There are two ways: 1. The task **type** is annotated `@CacheableTask` — every instance is cacheable by default. 2. A specific instance opts in via `task.outputs.cacheIf { <predicate> }`. ## Why full input/output declaration is the real requirement The cache is only correct if the cache key captures **everything** that influences the outputs. Gradle builds the key from the task's **declared** inputs: - `@Input` — scalar/serializable properties (strings, ints, enums). - `@InputFile` / `@InputFiles` / `@InputDirectory` — file inputs (hashed by content). - `@OutputFile` / `@OutputFiles` / `@OutputDirectory` — declared outputs (what gets stored/restored). - `@Classpath` / `@CompileClasspath` — file collections with classpath normalization. If a task reads a file or property it never declares, that value won't be in the key. Two runs with different real inputs can then collide on the same key, and Gradle will restore **wrong** outputs — a silent correctness bug. So "making a task cacheable" is mostly about **declaring inputs/outputs completely and accurately**, not just adding an annotation. ## Relocatability and normalization For a cache entry produced on one machine to be reusable on another, the key must not depend on machine-specific details like absolute paths. Gradle achieves this with **input normalization**: file inputs are tracked by **relative** path and content, classpaths ignore ordering/timestamps, and you can further normalize (e.g. `normalization { runtimeClasspath { ignore '...' } }`). `@CacheableTask` signals that the task's inputs are declared in a way that's safe to relocate. ## Minimum checklist ```kotlin @CacheableTask abstract class GenerateThing : DefaultTask() { @get:InputFiles @get:PathSensitive(PathSensitivity.RELATIVE) abstract val sources: ConfigurableFileCollection @get:Input abstract val version: Property<String> @get:OutputDirectory abstract val outputDir: DirectoryProperty } ``` The annotation makes the type opt in; the `@Input`/`@InputFiles`/`@OutputDirectory` declarations make the key correct and the result restorable.

  • What is the difference between a task reported as UP-TO-DATE and one reported as FROM-CACHE?
    UP-TO-DATE means inputs were unchanged since the last run in this same build directory, so Gradle skipped execution. FROM-CACHE means Gradle restored outputs from the build cache keyed by an input hash — it can apply across clean checkouts and other machines.
  • What happens if a task reads a file it never declares as an input?
    That file isn't part of the cache key, so two genuinely different runs can share a key and Gradle may restore stale or wrong outputs — a silent correctness bug. The fix is to declare it with the appropriate @Input* annotation.

Like memoizing a function: the cache is only correct if the function key includes every argument that affects the result. A hidden global read breaks it.

saying these in an interview costs you the question

  • Saying @CacheableTask alone makes caching safe, without mentioning complete input/output declaration.
  • Confusing build cache (cross-build/cross-machine) with up-to-date incremental checks (same build dir).
  • Claiming the cache key is based on output content rather than declared input content.

context

open as a page

Why do cacheable tasks need input path sensitivity and normalization, and how do you configure them?

level: middleimportance: must knowfreq 55%

basics

~10 s

Without normalization, absolute paths and irrelevant file metadata leak into the cache key, so entries miss across machines. @PathSensitive(RELATIVE) and classpath normalization make keys stable and relocatable.

open as a page

What is outputs.cacheIf {} and when would you use it instead of (or alongside) @CacheableTask?

level: middleimportance: should knowfreq 45%

basics

~20 s

outputs.cacheIf {} adds a runtime predicate that enables caching for a specific task instance only when it returns true. Use it when caching is worthwhile only under certain conditions, or to enable caching without owning the task type.

open as a page

Which input/output annotations does Gradle use to build a cacheable task's key, and what does @Internal mean for caching?

level: middleimportance: should knowfreq 40%

basics

~10 s

@Input, @InputFile(s)/@InputDirectory, @Classpath, @Nested feed the cache key; @OutputFile(s)/@OutputDirectory declare what's stored. @Internal marks a property that is neither — it's excluded from the key.

open as a page

A custom cacheable task gets cache hits locally but never on CI. As a senior engineer, how do you reason about and fix the cause?

level: seniorimportance: should knowfreq 35%

basics

~20 s

Local hits but cross-machine misses almost always mean the cache key isn't relocatable: absolute paths, machine-specific input content, or timestamps leak in. Fix with relative path sensitivity and input normalization so keys match across machines.

open as a page