Basic build caching

Basic build caching stores the outputs of build steps and reuses them when the inputs haven't changed.

L2 · DELEGATEDWhat this level takes
MUSTNot met, not at this level
  • Build caching is implemented (dependency cache, compilation cache)
  • Parallel build steps are configured (test and lint run concurrently)
  • Dedicated CI resources are allocated (not shared across all teams)
SHOULDExpected in practice, not required
  • Cache hit rate exceeds 60%
  • Build time has improved by at least 30% compared to uncached baseline
EVIDENCEHow you would check
  • Build cache configuration (Gradle build cache, npm cache, Docker layer cache)
  • CI pipeline configuration showing parallel step execution
  • Dedicated runner or resource pool configuration

What It Is

Basic build caching stores the outputs of build steps and reuses them when the inputs haven't changed. Instead of recompiling a Java class that was compiled identically yesterday, the build system fetches the compiled output from cache. Instead of re-downloading npm packages that are pinned to the same versions, the package manager reuses its local copy. Caching operates at multiple layers: compiler output caching, dependency download caching, test result caching, and Docker layer caching.

At L2, build caching is typically local and single-layer: Gradle's local build cache, npm's node_modules cache, pip's wheel cache, or Maven's local repository. These are the "first optimization layer" - low effort to enable, immediate impact on warm builds, but limited effectiveness across machines or concurrent agents. A developer's local cache doesn't help a CI runner that starts fresh, and a cache in one worktree doesn't benefit another worktree running in parallel.

The most impactful caching wins at L2 are dependency caching in CI and task output caching for compilation. Dependency downloads are often a surprising fraction of CI build time - downloading 500MB of npm packages or Maven dependencies on every CI run can add 2-5 minutes that caching eliminates entirely. Compilation output caching makes the second build of a given source state essentially free, which is directly valuable for agent iteration loops where the same baseline code is compiled many times.

L2 caching is not the end state for AI-enabled development. Local caches don't share results across developer machines or CI runners. Concurrent agents in different worktrees can't benefit from each other's compilation work. But L2 caching is the necessary foundation for L3 remote execution - you need to understand cache keying, cache hit rates, and cache invalidation before layering on distributed builds.

Why It Matters

  • Dependency caching alone can cut CI time by 30-50% - downloading dependencies is often the largest single CI step, and it's entirely eliminable with proper caching
  • Warm build times drop to near-zero for unchanged code - a developer making a one-line change in a cached build sees only the compilation time for the changed file, not the full project
  • Agent iteration loops benefit immediately - an agent iterating on a single module sees all other modules as instant cache hits; only the module under change is recompiled
  • Cache configuration is low-risk, high-reward - unlike remote execution infrastructure, local caching requires no external services and has minimal failure modes
  • Establishes the measurement baseline for L3 - cache hit rates and build time profiles measured at L2 inform the ROI calculation for investing in Bazel or remote execution

Getting Started

  1. Enable Gradle local build cache - Add org.gradle.caching=true to gradle.properties. Run the same build twice and check the second run's task output: tasks marked FROM-CACHE are cache hits. A 70%+ cache hit rate on a second build is a reasonable L2 target.
  2. Cache CI dependencies - In GitHub Actions, add a cache step for your dependency directories: ~/.gradle/caches for Gradle, ~/.m2/repository for Maven, node_modules or ~/.npm for Node.js, ~/.cache/pip for Python. Use the lock file as the cache key: changes to package-lock.json or pom.xml invalidate the cache, everything else reuses it.
  3. Enable npm/yarn/pnpm caching - Most CI platforms have built-in actions for Node.js dependency caching. actions/setup-node with cache: npm handles cache key generation and restoration automatically. Verify that the cache is being hit by checking CI logs for "Cache restored" messages.
  4. Cache Docker layers - If your CI builds Docker images, configure BuildKit with a cache backend. For GitHub Actions, use cache-from: type=gha and cache-to: type=gha,mode=max in your docker/build-push-action step. Docker layer caching can cut image build time by 60-80% when only the application layer changes.
  5. Measure cache effectiveness - Add cache hit rate to your CI metrics. Gradle Build Scan reports this automatically. For npm and Docker, parse CI logs to count cache hits vs. misses. Target 80%+ cache hit rates for dependency caches and 60%+ for compilation caches.
  6. Handle cache poisoning - Define a process for invalidating caches when they're stale or incorrect. For Gradle, this means bumping the cacheVersion in gradle.properties. For CI dependency caches, it means including a manual bust key in the cache key string. Document this process so developers know how to respond when a build succeeds locally (cache hit) but fails in CI (cache miss exposing a real bug).
TIP

The biggest CI caching win that most teams miss is splitting dependency installation from compilation in CI pipelines. If npm install is in the same job as npm run build, many CI platforms cache less effectively. Separate them into distinct steps so the cache can be populated and restored independently.

Common Pitfalls

Caching with overly broad cache keys. A cache key that changes when any file in the repository changes provides little benefit. Use specific keys: the hash of your lock file for dependencies, the hash of source files for compilation outputs. Overly broad keys result in cache misses even when nothing relevant changed.

Not validating cache correctness. A build cache that returns incorrect outputs is worse than no cache - it causes hard-to-diagnose failures where the build reports success but the artifact is stale. Test cache correctness explicitly: make a change, build, revert the change, build again. The second build should produce exactly the same output as the pre-change build.

Sharing caches without considering security. CI caches can be poisoned if untrusted code runs in a CI environment with write access to the cache. On GitHub Actions, caches from pull requests from forks can potentially poison the main branch cache. Separate caches by branch or restrict cache write access to trusted branches.

Expecting local caches to solve CI performance. A developer's warm local Gradle cache doesn't help the CI runner that's starting cold. CI caching requires explicit CI-side cache configuration. Don't assume that because builds are fast locally they'll be fast in CI.

Not monitoring cache size and eviction. Build caches grow unbounded without eviction policies. A Gradle local cache that's 50GB is not more useful than a 10GB cache, but it does consume significant disk space and can slow down cache lookups. Set maximum cache sizes in gradle.properties (org.gradle.cacheUsePersistence=ENABLED with a size limit) and monitor cache growth in CI.

How Different Roles See It

BobHEAD OF ENGINEERING

Bob has approved an initiative to improve developer productivity and wants to show a quick win. His DevEx lead has identified that CI pipeline time averages 12 minutes, with 4 minutes spent downloading dependencies on every run. This is a clear target: dependency caching should eliminate those 4 minutes immediately.

What Bob should do: Bob should have his infrastructure team implement CI dependency caching for the main repositories this week - it's a half-day task with immediate measurable impact. He should track average CI time before and after, and share the result with the team as a productivity win. This builds momentum for the more significant investment in L3 build infrastructure. Bob should also ask whether agent-generated CI runs are being measured separately - if they are, the before/after comparison for agent iteration loop time will be even more dramatic than for human PR builds.

SarahPRODUCTIVITY LEAD

Sarah has been collecting CI time data and sees that the distribution is bimodal: fast builds under 4 minutes (cache hits) and slow builds over 12 minutes (cache misses). The slow builds happen predictably when lock files change or new CI runners start. She wants to narrow this distribution.

What Sarah should do: Sarah should work with the infrastructure team to understand what's causing the slow tail. For each major source of cache misses - lock file changes, cold CI runners, first build on a new branch - there's a specific caching strategy. Lock file changes should trigger cache refresh automatically. Cold CI runners should be pre-warmed with a daily scheduled build that populates the cache. New branches should seed from the main branch cache rather than starting cold. Each of these is a targeted intervention that narrows the build time distribution and makes CI time more predictable for both developers and agents.

VictorSTAFF ENGINEER - AI CHAMPION

Victor has implemented all local and CI caching and is getting consistent warm build times of 45 seconds for his main module. But he's realized that local caches are not shared between his 4 parallel worktrees - each worktree maintains its own Gradle cache, so the same compilation work happens 4 times instead of once.

What Victor should do: Victor should configure a local remote cache server to share between his worktrees. The Gradle Build Cache Docker image runs as a local HTTP server that all worktrees can point at. Each worktree's gradle.properties gets org.gradle.caching=true and a remote cache URL pointing at the local server. With this setup, the first worktree to compile a given module populates the local server, and all other worktrees get cache hits. Victor should measure the shared cache hit rate across his 4 worktrees and target 70%+ - this is the configuration that makes local parallel agents nearly as fast as a single agent for shared infrastructure code.

Where does your team actually sit on this?

This guide describes one level of one area. Run the assessment to place your team across all 16 areas, see which gates you have passed, and get a report you can take to your stakeholders.

Start the assessment