Topic 1.3
Caching & Speeding Up Builds
In one line
Re-downloading every dependency and rebuilding everything from scratch on every single run is genuinely wasteful — caching lets a pipeline reuse work from a previous run whenever nothing relevant has actually changed.
Think of it like this
Repacking your entire suitcase from an empty dresser every single morning, instead of just adding today's one new item to what's already packed — caching is keeping the parts that genuinely haven't changed (your dependencies, most of the time) rather than redoing that work from absolute zero on every single pipeline run.
Key ideas
- 01
Without caching, EVERY pipeline run starts from a genuinely fresh, empty workspace on a hosted runner — every single dependency (Maven's
~/.m2repository, Gradle's own cache,node_modulesfor a JavaScript project) gets downloaded again from scratch, every single time, even when the actual dependency list hasn't changed at all between runs. - 02
A CACHE KEY determines whether a previously-saved cache can be reused — commonly derived from a hash of the dependency-lock file itself (
pom.xml,package-lock.json) specifically, since dependencies only genuinely need re-downloading when that file actually changes; an unchanged lock file means the exact same cache can be safely reused run after run. - 03
actions/cache(GitHub Actions' own caching action) saves a specified directory (Maven's~/.m2, or anode_modulesfolder) keyed by that hash, and RESTORES it automatically on a later run if the key matches — a cache MISS (the key changed, meaning dependencies genuinely changed) correctly falls back to a full, fresh download, exactly as it should. - 04
This directly connects to Phase 0.3's build systems — Gradle's own INCREMENTAL BUILD system (skipping tasks whose inputs haven't changed, visible as
UP-TO-DATEin its own console output) is conceptually the exact same idea as CI-level dependency caching, just operating at the level of individual build TASKS rather than the whole dependency download step. - 05
Caching genuinely speeds up pipelines significantly (often turning a several-minute dependency download into a few-second cache restore), but a STALE or INCORRECTLY-KEYED cache can cause genuinely confusing failures — a cache that was keyed too broadly (not actually tied to the real dependency file) might restore OUTDATED dependencies silently, which is exactly why the cache key must genuinely reflect what would actually invalidate the cache's correctness.
In your stack
- →
For a Node project, caching
node_modules(or, more commonly and more safely, npm's own global cache directory) keyed onpackage-lock.json's hash provides the identical speedup — a pipeline that would otherwise runnpm installfrom scratch on every single run instead restores from cache almost instantly whenever the lock file hasn't changed.
Code & diagrams
The lock file's hash decides everything — unchanged means a near-instant restore; changed means a correct, fresh download.
Maven dependency caching, keyed correctly on the actual dependency declaration file.
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with: { java-version: "21", distribution: "temurin" }
- name: Cache Maven dependencies
uses: actions/cache@v4
with:
path: ~/.m2/repository
key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
restore-keys: |
${{ runner.os }}-maven-
- run: mvn clean packageExplain it without notes
Why is a dependency-lock file's hash a genuinely good choice for a cache key, rather than something simpler like the current date?
What real risk does an incorrectly-configured, too-broad cache key introduce?
Practice
Add dependency caching to a real pipeline (adjusted for your build tool) and compare the pipeline's total run time before and after — confirm a subsequent run with an unchanged lock file shows a clear cache hit and a meaningfully faster total time.
Deliberately change your dependency file (add or update one dependency) and confirm the next pipeline run shows a cache MISS, correctly re-downloading fresh dependencies.
Trade-offs
- ↔
Caching trades a small amount of storage (the cache itself needs to be saved and stored between runs, usually with a real size limit and eventual expiry on hosted platforms) for a genuinely significant, often dramatic reduction in pipeline run time — for any project with a non-trivial number of dependencies, this trade is close to universally worth making, which is exactly why dependency caching is one of the first optimizations most real teams add to a working pipeline.
Done when you can
I understand what a cache key is and why it should be derived from a dependency-lock file's hash.
I can add dependency caching to a real pipeline and measure the resulting speedup.
I understand the real risk of an incorrectly-configured, too-broad cache key.