skip to content

Why does a change to buildSrc slow down or invalidate caching for the whole build, and what does that imply?

level: middleimportance: must knowfreq 55%

answer

  1. buildSrc output on every script classpath
  2. classpath is part of the cache key
  3. conservative, global invalidation
  4. keep buildSrc small & stable
  5. split to build-logic for big repos

basics

~20 s

buildSrc output is on every build script's classpath. Changing it changes that classpath, so Gradle must recompile and re-evaluate the build scripts and busts the build-script / configuration cache — making the next build slower everywhere.

solid answer

~40 s

Gradle puts `buildSrc`'s compiled output on the classpath of **every** build script. The build-script classpath is part of the cache key for build-script compilation and for the configuration cache. So when you edit anything under `buildSrc/src`, that classpath changes, Gradle recompiles `buildSrc` and then conservatively recompiles/re-evaluates the build scripts that depend on it — and invalidates the configuration cache for the run. The practical implications: (1) keep `buildSrc` small and stable — frequent churn there taxes every developer's incremental builds; (2) avoid putting volatile or fast-moving logic there; (3) for very large builds, prefer a separate `includeBuild("build-logic")` composite build whose plugins are versioned/cached more granularly. The cost is unavoidable by design — it is the price of zero-ceremony global availability.

code

bash · 6 lines
bash
# Observe the cost: a no-op edit to buildSrc forces re-evaluation
touch buildSrc/src/main/kotlin/com/acme/Constants.kt
./gradlew help --configuration-cache
# -> "Configuration cache entry stored" then on next run with a buildSrc edit:
# -> "Calculating task graph as configuration cache cannot be reused because
#     a build logic input has changed"

go deeper

for a junior

Know that editing buildSrc makes the next build slower because it is on every build script's classpath.

for a middle

Explain the classpath-as-cache-key mechanism and the global invalidation, plus keep-it-small advice.

for a senior

Distinguish build-script/config cache from task-output cache and reason about when to split into build-logic.

for a principal

Set org guidance: build-speed budgets, buildSrc churn policies, and composite-build migration thresholds.

## The mechanism Gradle evaluates a build in phases: **initialization** (settings, included builds, `buildSrc`), **configuration** (run every build script's body to build the task graph), then **execution**. Build scripts are themselves compiled Kotlin/Groovy programs. To compile and run them, Gradle needs a **build-script classpath** — and `buildSrc`'s output JAR is part of that classpath for every script. Gradle caches the *compiled* build scripts and (with the configuration cache enabled) the entire configured task graph. The **cache key** includes the build-script classpath. So: ``` edit buildSrc/src/main/kotlin/Foo.kt -> buildSrc recompiles -> build-script classpath content changes -> compiled-build-script cache entry is stale -> Gradle recompiles affected build scripts -> configuration cache for that run is invalidated ``` Even a one-character change to a helper used by nobody changes the JAR, hence the classpath, hence the cache. ## Why "the whole build" Because the classpath is **global** — added to *every* build script — Gradle cannot cheaply prove which scripts are actually affected, so it invalidates conservatively. The convenience (available everywhere with no wiring) is exactly what makes the blast radius large. ## Implications & mitigations - **Keep buildSrc lean and stable.** Constants, a few task types, convention plugins — not a sprawling, frequently-edited codebase. - **Don't put churny logic there.** Anything you tweak daily multiplies build cost. - **Split out a build-logic composite build** for big repos: `includeBuild("build-logic")` lets you structure plugins as separately-built artifacts; editing one plugin's module can have a tighter recompilation scope, and the boundary is explicit. (That's the sibling 'Included build-logic vs buildSrc' topic — here the point is just *why* you'd reach for it.) - **Measure with `--scan`/`--profile`** to see whether buildSrc recompilation is a real bottleneck before optimizing. ## What it is NOT This is *not* about the **build cache** for task outputs of your application code — your `:app:compileKotlin` outputs are still cacheable. It is the *build-script* compilation cache and the **configuration cache** that get invalidated when buildSrc changes.

  • Does a buildSrc change invalidate the task-output build cache for application modules?
    Not inherently. It invalidates build-script compilation and the configuration cache; application task outputs remain cacheable unless their own inputs changed. But re-configuration still costs time.
  • How would you reduce this overhead in a very large monorepo?
    Move shared plugins into a separate includeBuild("build-logic") composite build with finer module boundaries, and keep buildSrc minimal so edits are rare.

saying these in an interview costs you the question

  • Confusing the build-script/configuration cache with the task-output build cache.
  • Claiming buildSrc changes are incrementally scoped to only the affected scripts (Gradle invalidates conservatively).
  • Suggesting you should never use buildSrc because of caching — it is fine when kept small and stable.

context