skip to content

Local State and Destroyable Outputs

The less common task annotations — output collections, @LocalState, @Destroys, @ReplacedBy — and what each tells Gradle about scheduling and caching. Interviewers ask because @Destroys is why clean is never run concurrently with a build.

on this pageshow

questions

5

What does the @LocalState annotation declare on a task property, and why would you use it instead of @OutputDirectory?

level: middleimportance: should knowfreq 30%

answer

  1. intermediate, machine-specific scratch
  2. tracked for cleanup, excluded from cache packing
  3. deleted on cache-hit restore
  4. incremental-compilation analysis files
  5. not an output, not portable

basics

~10 s

@LocalState marks a directory or file holding a task's intermediate, machine-specific state. Gradle tracks it for up-to-date checks but does NOT store it in the build cache, because it shouldn't be shared across machines.

solid answer

~40 s

`@LocalState` declares a property pointing at intermediate state a task produces that is **not** a shareable output — typically incremental-compilation analysis files or scratch data tied to the local machine/filesystem. Like outputs, Gradle removes local state when the task is loaded from the build cache (so a cached run doesn't leave stale local state behind), but unlike `@OutputDirectory`, local state is **never packed into a cache entry**. That keeps cache entries portable: machine-specific paths or absolute references inside local state won't pollute what other machines download. Use it instead of `@OutputDirectory` when the directory speeds up *re-runs on this machine* but would be meaningless or harmful if shipped to another machine via the cache.

code

kotlin · 16 lines
kotlin
@CacheableTask
abstract class GenerateCode : DefaultTask() {
    @get:InputDirectory
    @get:PathSensitive(PathSensitivity.RELATIVE)
    abstract val sources: DirectoryProperty

    @get:OutputDirectory
    abstract val generated: DirectoryProperty

    // Incremental index that speeds local re-runs but must NOT be cached/shared.
    @get:LocalState
    abstract val analysisDir: DirectoryProperty

    @TaskAction
    fun run() { /* read analysisDir, write generated, update analysisDir */ }
}

go deeper

for a junior

Know that @LocalState marks intermediate, machine-local scratch data that Gradle tracks but does not put in the build cache.

for a middle

Explain the two behaviors: excluded from cache packing AND deleted on cache-hit restore, and give the incremental-compilation example.

for a senior

Contrast with @OutputDirectory (portable, packed) and @Internal (ignored), and explain why portability of cache entries motivates the distinction.

for a principal

Discuss governance: when a custom plugin's task leaks machine-specific paths into cache entries, prescribe @LocalState as the fix and reason about cross-machine cache hit rates.

## What 'local state' means A task in Gradle declares **inputs** (things that, when changed, make the task stale) and **outputs** (the artifacts it produces). The build cache packs declared outputs into a cache entry keyed by a hash of the inputs, so another machine — or a later build — can download outputs instead of re-running the task. Some tasks also produce **intermediate state** that: - speeds up the *next* run on the same machine (e.g. an incremental-compilation analysis cache), but - is **not a real output** consumers want, and - is **not portable** — it may embed absolute paths or be tied to this filesystem. Declaring such a directory with `@OutputDirectory` would be wrong: it would get packed into the cache and shipped to other machines, where it is at best useless and at worst corrupting. Declaring it as a plain field (untracked) is also wrong: Gradle wouldn't clean it up when restoring from cache, leaving stale state behind. `@LocalState` is the middle ground. ## What @LocalState does ``` @get:LocalState abstract val analysisDir: DirectoryProperty ``` 1. **Tracked for cleanup, not for portability.** When the task's outputs are restored from the build cache (a cache hit), Gradle **deletes** any registered local-state directories first. This prevents a stale local-state cache from being mixed with freshly-downloaded outputs. 2. **Never stored in the cache entry.** Local state is excluded from what gets packed, so cache entries stay machine-independent. 3. **Not an output for up-to-date purposes in the consumer sense** — it does not become an input to downstream tasks. ## When to reach for it The canonical example is incremental compilation: the Java/Kotlin compile tasks keep an analysis/mapping file that makes the *next* incremental compile fast, but it's local scratch. Gradle's own `JavaCompile` uses local state for exactly this. If you write a code-generation or compilation-style task that keeps a private incremental index, declare it `@LocalState`. ## Relationship to @CacheableTask `@LocalState` only matters for **cacheable** tasks (tasks annotated `@CacheableTask` or otherwise cache-enabled). For a non-cacheable task there is no cache entry to exclude it from, and no cache-restore step to clean it during, so the annotation has no practical effect.

  • What happens to a @LocalState directory when the task's outputs are loaded from the build cache?
    Gradle deletes the local-state directory before/while restoring outputs, so stale local state isn't combined with freshly downloaded cached outputs.
  • Does @LocalState do anything for a task that isn't cacheable?
    Effectively no. With no cache entry to exclude it from and no cache-restore cleanup step, the annotation has no practical effect on a non-cacheable task.

Output files are the finished cake you can mail to anyone; local state is the flour-dusted mixing bowl that helps you bake faster next time but is useless to ship.

saying these in an interview costs you the question

  • Saying local state IS stored in the build cache — it is explicitly excluded.
  • Claiming @LocalState makes the directory a task output usable by downstream tasks.
  • Confusing it with @Internal (untracked) — local state is still cleaned on cache restore, @Internal is fully ignored.

context

open as a page

When and how would you use @OutputFiles or @OutputDirectories (the map-accepting forms) instead of single @OutputFile/@OutputDirectory properties?

level: middleimportance: should knowfreq 28%

basics

~10 s

Use @OutputFiles/@OutputDirectories when a task produces multiple, dynamically-keyed outputs. Backing the property with a Map<String, File> gives each output a stable identity key, which Gradle uses for reliable up-to-date checks and overlapping-output handling.

open as a page

A custom cacheable task produces some real outputs, keeps an incremental scratch directory, and a separate task wipes that scratch directory on demand. Walk through which annotations you'd use where, and why.

level: seniorimportance: should knowfreq 18%

basics

~10 s

Real artifacts → @OutputFiles/@OutputDirectories. The incremental scratch directory → @LocalState (tracked, not cached). The wipe task's deleted path → @Destroys so it never races the producer under --parallel. Inputs stay @Input/@InputFiles with path sensitivity.

open as a page

What is the @Destroys annotation for, and how does it affect task scheduling and parallel execution?

level: seniorimportance: should knowfreq 22%

basics

~20 s

@Destroys marks a path a task removes (e.g. a clean task deleting build/). Gradle uses it to schedule destroyer tasks so they never overlap with tasks that produce or consume those same paths, preventing data races in parallel builds.

open as a page

What is @ReplacedBy used for, and why can't you just rename an input/output property and replace its annotation?

level: seniorimportance: nice to knowfreq 12%

basics

~20 s

@ReplacedBy marks a deprecated getter that has been superseded by a new property, telling Gradle the old getter is NOT an input/output to track. It avoids double-counting the same value during a backward-compatible API migration on a task type.

open as a page