skip to content

Incremental Work Avoidance

Everything that lets Gradle avoid work: up-to-date checks, incremental task execution, path sensitivity, and classpath normalization. Interviewers ask because these are the knobs that decide whether caching ever actually hits.

on this pageshow

explore

questions

25

What does the @Classpath input annotation do, and why would you use it instead of @InputFiles for a collection of jars?

level: juniorimportance: must knowfreq 55%

answer

  1. order significant, name/path ignored
  2. content hash of jar entries
  3. @InputFiles uses absolute path by default
  4. relocatable + cacheable
  5. @CompileClasspath = ABI only

basics

~10 s

@Classpath marks a task input as a classpath: Gradle hashes the file contents in order but ignores file names and paths. @InputFiles treats each file's absolute path as significant, so it over-invalidates.

solid answer

~40 s

`@Classpath` tells Gradle that a property is a *classpath* — an ordered list of jars or class directories. For up-to-date checks and the build cache, Gradle hashes the **content** of each entry, respects their **order** (classpath order is semantically meaningful), but **ignores the names and paths** of the jar files. `@InputFiles` (or `@InputFiles @PathSensitive`) instead fingerprints individual files where name/path can matter, so a renamed or relocated jar — common with dependency caches on different machines — needlessly invalidates the task. Using `@Classpath` makes the task cacheable and relocatable across machines, which is exactly what you want for `compile`/`runtime` classpaths. There's also `@CompileClasspath` for the narrower case where only the API (public signatures) matters.

code

kotlin · 7 lines
kotlin
abstract class BundleTask : DefaultTask() {
    @get:Classpath
    abstract val runtimeJars: ConfigurableFileCollection

    @get:OutputFile
    abstract val bundle: RegularFileProperty
}

go deeper

for a junior

Know that @Classpath ignores file names/paths but keeps order, so tasks stay up-to-date when only the path changes.

for a middle

Explain content-hashing of jar entries and why this makes tasks relocatable and cacheable across machines.

for a senior

Contrast @Classpath vs @CompileClasspath, and explain how default path sensitivity on @InputFiles causes cache misses.

for a principal

Frame it as a relocatability/cache-correctness concern across a fleet of CI agents and developer machines.

## What a classpath input is A *classpath* is an **ordered** sequence of jars and class directories handed to the JVM or to `javac`. Two properties make it special compared with an arbitrary file collection: 1. **Order matters** — the first class found on the classpath wins, so reordering can change behaviour. 2. **Names/paths do not matter** — `guava-32.jar` resolved into `~/.gradle/caches/...` on one machine and a different absolute path on a CI agent is the *same* dependency. The file name and directory are accidents of where the dependency cache lives. ## How Gradle fingerprints inputs Gradle computes a **fingerprint** of every task input to decide up-to-date status and to form the build-cache key. By default `@InputFiles` uses `ABSOLUTE` path sensitivity unless told otherwise, meaning the absolute path of each file is part of the hash. For a classpath that's wrong: it makes the task non-relocatable (cache misses between machines) and over-sensitive to harmless renames. ## @Classpath Annotating a property with `@Classpath`: - Hashes the **content** of each entry. - Preserves **order** of the entries. - **Ignores** the file names and the paths. - For jar files it hashes the *contents of the jar* (the entries inside), not the jar bytes, so a rebuilt jar with a different timestamp but identical classes produces the same fingerprint. This is the correct annotation for a *runtime* classpath input. ## @CompileClasspath `@CompileClasspath` is a stricter sibling for *compile* classpaths: it ignores everything that can't affect compilation — method bodies, private members, resources inside jars, debug info — and fingerprints only the **ABI** (public type signatures). Changing a method body of an upstream library therefore does **not** invalidate downstream compilation, only the recompiled jar's own consumers' runtime tasks. ## Code ```kotlin abstract class BundleTask : DefaultTask() { @get:Classpath abstract val runtimeJars: ConfigurableFileCollection @get:CompileClasspath abstract val apiJars: ConfigurableFileCollection @get:OutputFile abstract val bundle: RegularFileProperty @TaskAction fun bundle() { /* ... */ } } ``` Built-in tasks already do this: `JavaCompile.classpath` is `@CompileClasspath`, and runtime-oriented tasks use `@Classpath`. You only reach for these annotations on **custom** tasks.

  • Why does @Classpath hash the contents of a jar rather than the jar file's bytes?
    Because jar files embed timestamps and ordering that change on every rebuild even when the compiled classes are identical. Hashing the logical entries inside makes the fingerprint stable across rebuilds.
  • Does @Classpath ignore the order of the entries?
    No — order is preserved and significant, since classpath order determines which class wins when names collide. Only names and paths are ignored.

Think of a classpath like a stack of numbered transparencies: the order you stack them changes the picture, but it doesn't matter what's scribbled on the back (the file name) or which drawer they came from (the path).

saying these in an interview costs you the question

  • Saying @Classpath ignores ordering — it does not; order is significant.
  • Claiming @Classpath and @InputFiles are interchangeable for jars.
  • Thinking @Classpath hashes the raw jar bytes (it hashes the entries inside).

context

open as a page

What are task inputs and outputs in Gradle, and how do you declare them for an ad-hoc task using the runtime API?

level: juniorimportance: must knowfreq 70%

basics

~10 s

Inputs are the files/values a task reads; outputs are what it produces. For ad-hoc tasks you declare them at runtime via task.inputs.file()/dir()/property() and task.outputs.file()/dir() so Gradle can track changes.

open as a page

What does it mean when Gradle marks a task as UP-TO-DATE, and how does Gradle decide that?

level: juniorimportance: must knowfreq 70%

basics

~10 s

UP-TO-DATE means Gradle skipped the task because its inputs and outputs haven't changed since the last run. Gradle compares snapshots of the declared inputs/outputs; if they match, it reuses the previous result.

open as a page

How does @CompileClasspath differ from @Classpath, and how does ABI-based fingerprinting help avoid recompilation?

level: middleimportance: must knowfreq 50%

basics

~10 s

@CompileClasspath fingerprints only the public API (ABI) of jars — class/method/field signatures — ignoring method bodies, private members and resources. So changing only an implementation detail upstream doesn't invalidate downstream compilation.

open as a page

How do you configure runtime classpath normalization to ignore a volatile file like build-info.properties, and why is it needed?

level: middleimportance: must knowfreq 40%

basics

~10 s

In settings.gradle(.kts) use normalization { runtimeClasspath { ignore 'build-info.properties' } }. It strips that file from the runtime-classpath fingerprint so a volatile, build-stamped entry doesn't cause needless cache misses or out-of-date tasks.

open as a page

In an incremental task action, why must you handle ChangeType.REMOVED specially, and what goes wrong if you don't?

level: middleimportance: must knowfreq 45%

basics

~10 s

Because Gradle only tells your action about changed files, you alone must delete outputs for inputs that were removed. Skip it and stale, orphaned output files pile up and pollute later builds.

open as a page

What does the InputChanges API give a task action that ordinary up-to-date checking does not, and when does a task receive it?

level: middleimportance: must knowfreq 55%

basics

~10 s

Up-to-date checking only decides skip-or-rerun for the whole task. InputChanges tells the action exactly which input files were added, modified, or removed, so it can reprocess only those instead of everything.

open as a page

How exactly do missing or incorrect input/output declarations break Gradle's up-to-date checks and build cache?

level: middleimportance: must knowfreq 65%

basics

~20 s

Gradle decides up-to-date and the cache key from DECLARED inputs/outputs only. A missing input means changes go undetected (stale results); a missing output means the cache can't store/restore it, and over-declaring causes needless re-runs and cache misses.

open as a page

What is path sensitivity in Gradle, and why does it matter for up-to-date checks and the build cache?

level: middleimportance: must knowfreq 45%

basics

~20 s

Path sensitivity tells Gradle whether a file input's path (not just its content) is part of the cache key. Choosing RELATIVE or NAME_ONLY instead of ABSOLUTE lets the same content reuse cached results across different machines or directories.

open as a page

Which annotations make a task's inputs and outputs participate in up-to-date checks, and what happens if you forget to declare one?

level: middleimportance: must knowfreq 65%

basics

~20 s

Use @Input for values and @InputFile/@InputFiles for file inputs, and @OutputFile/@OutputDirectory for outputs. If you forget one, Gradle can't see it, so the task may show UP-TO-DATE even though that thing changed — a stale build.

open as a page

Walk through the FileChange objects returned by InputChanges.getFileChanges: what does each ChangeType mean and what information does a FileChange carry?

level: juniorimportance: should knowfreq 32%

basics

~10 s

getFileChanges returns an iterable of FileChange. Each has a ChangeType (ADDED, MODIFIED, or REMOVED) and the file plus its normalized path, telling you what happened to that one input file.

open as a page

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%

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.

open as a page

In Gradle's console output, what's the difference between UP-TO-DATE, NO-SOURCE, FROM-CACHE, and a task with no label?

level: middleimportance: should knowfreq 40%

basics

~20 s

No label = the task executed. UP-TO-DATE = skipped because inputs/outputs didn't change. NO-SOURCE = skipped because it had no input files at all. FROM-CACHE = its outputs were restored from the build cache instead of being computed.

open as a page

You're authoring a custom task that takes a set of jars. How do you decide between @Classpath, @CompileClasspath, and @InputFiles with @PathSensitive for that property?

level: seniorimportance: should knowfreq 30%

basics

~20 s

Use @CompileClasspath if you only need the jars' public API, @Classpath if you need real runtime bytes but order matters and names/paths don't, and @InputFiles with an explicit @PathSensitive (RELATIVE/NAME_ONLY/NONE) for non-classpath file inputs where order is irrelevant.

open as a page

A cacheable task that consumes a runtime classpath always reports out-of-date even when nothing meaningful changed. How would you diagnose and fix it?

level: seniorimportance: should knowfreq 35%

basics

~10 s

Run with --info or build-cache debug to see which input changed, find the volatile classpath entry (e.g. a timestamped properties file or manifest attribute), then add a runtimeClasspath normalization ignore/metaInf.ignoreAttribute rule in settings.

open as a page

What situations cause Gradle to run an InputChanges-aware task in non-incremental mode (isIncremental == false)?

level: seniorimportance: should knowfreq 38%

basics

~10 s

The first run, a change to a non-@Incremental input or input property, output history being unavailable, or outputs modified outside Gradle. Then getFileChanges reports every file as ADDED.

open as a page

A task keeps producing stale results even after you change a source file. How do you confirm and fix a missing input declaration?

level: seniorimportance: should knowfreq 38%

basics

~10 s

Run with --info to see Gradle report the task UP-TO-DATE despite your change — that confirms the changed file isn't a declared input. Add it via inputs.file()/dir()/property() so its content joins the fingerprint.

open as a page

What goes wrong when ad-hoc input/output declarations are configured eagerly or with hardcoded paths, and how do lazy Providers help?

level: seniorimportance: should knowfreq 40%

basics

~10 s

Eager File paths and values captured at configuration time can be wrong, miss task-dependency inference, or read stale config. Using lazy Providers/Property and layout.buildDirectory defers resolution and links producer→consumer tasks automatically.

open as a page

Compare RELATIVE, NAME_ONLY, and NONE path sensitivity. How do you choose the right one for a given file input?

level: seniorimportance: should knowfreq 35%

basics

~20 s

RELATIVE keys on the path relative to the input root; NAME_ONLY keys only on the file name; NONE keys only on content. Pick the loosest one where renaming or moving a file would NOT change the task's correct output.

open as a page

How does runtime classpath normalization help cache hits, and how do you configure it to ignore volatile files inside artifacts?

level: seniorimportance: should knowfreq 30%

basics

~20 s

Runtime classpath normalization lets you tell Gradle to ignore specific files (like build-stamped property files) when fingerprinting a runtime classpath, so a harmless change inside a jar doesn't bust up-to-date or cache for tasks like test.

open as a page

A task keeps re-running every build even though nothing seems to change. How do you diagnose why it's never UP-TO-DATE?

level: seniorimportance: should knowfreq 45%

basics

~20 s

Find which declared input or output changes each run. Use --info to see Gradle's reason for re-running, or a build scan's timeline. Common causes: a timestamp/absolute path baked into an input, or an output written outside the declared location.

open as a page

What does doNotTrackState() do, when would you use it, and what are the consequences?

level: seniorimportance: should knowfreq 30%

basics

~10 s

doNotTrackState() tells Gradle a task has no reliable up-to-date state, so Gradle skips snapshotting its inputs/outputs and always runs it. The cost: the task never shows UP-TO-DATE and is never cached.

open as a page

A task you didn't write (e.g. a third-party plugin's task or `test`) keeps missing the remote cache despite no real change. How do you diagnose and fix it via normalization/path sensitivity from the consumer side?

level: principalimportance: should knowfreq 22%

basics

~20 s

Use build-cache debug logging or the build scan to compare input fingerprints between two runs, find which input changed (often an absolute path or a build-stamped file in a jar), then stabilize it with runtime classpath normalization or by adjusting the input's path sensitivity.

open as a page

What does @IgnoreEmptyDirectories do, and when does ignoring empty directories matter for incremental builds?

level: middleimportance: nice to knowfreq 15%

basics

~10 s

@IgnoreEmptyDirectories tells Gradle to leave empty directories out of an input's fingerprint, so stray or environment-created empty folders don't change up-to-date status or the cache key.

open as a page

How does getFileChanges behave when a task has several @Incremental inputs, and how should the action query and respond to changes across them?

level: seniorimportance: nice to knowfreq 22%

basics

~10 s

Call getFileChanges once per @Incremental input property; each call returns only that input's changes. If isIncremental is true you can process each independently; if false, every input reports all files as ADDED.

open as a page