Incremental builds: only changed fragments
Incremental builds are a build strategy where only the files, modules, or packages that have changed since the last build are recompiled, and only the tests that cover changed code are re-executed.
- Incremental builds run only changed modules or fragments
- Remote caching is implemented (Bazel remote cache, EngFlow, Gradle Enterprise)
- CI completes in under 5 minutes (median)
- P95 CI duration is under 8 minutes
- Build system supports hermetic builds (reproducible outputs regardless of machine)
- CI run duration dashboard showing median under 5 minutes
- Remote cache configuration and cache hit rate metrics
- Build configuration showing incremental/changed-only targeting
- Infrastructure L2 (Build System) - basic build caching must be in place before remote caching is meaningful
What It Is
Incremental builds are a build strategy where only the files, modules, or packages that have changed since the last build are recompiled, and only the tests that cover changed code are re-executed. Rather than rebuilding everything from scratch on every commit - the default behavior of most CI pipelines - incremental builds compute the minimal set of work required to validate a change.
The mechanism varies by build system. In Gradle, incremental compilation tracks which Java source files have changed and recompiles only those files and their transitive dependents. In TypeScript, tsc --incremental maintains a tsconfig.tsbuildinfo file that tracks which source files changed and recompiles accordingly. In Bazel, incremental builds are a core property: every build action declares its inputs, and Bazel only re-executes actions whose inputs have changed. In monorepo setups using tools like Turborepo (JavaScript), Nx (JavaScript/TypeScript), or Pants (Python/Java), the build graph is explicit and only the affected packages are built and tested.
For AI agents generating many small, focused commits, incremental builds are disproportionately valuable. An agent fixing a bug in a single module typically changes 2-3 files in a codebase of thousands. Without incremental builds, CI rebuilds and retests the entire codebase for that 3-file change. With incremental builds, CI rebuilds only the changed module and the modules that depend on it. On a large monorepo, this can reduce CI time from 8 minutes to 45 seconds for a typical agent-generated change. The impact compounds: agents that generate 20 small changes per hour get a 10x CI speedup compared to agents working against full-rebuild CI.
The relationship between incremental builds and test impact analysis is complementary. Incremental builds reduce the compilation and build time by only recompiling changed code. Test impact analysis reduces the test execution time by only running tests affected by changed code. Mature CI pipelines at L3-L4 implement both: incremental compilation followed by targeted test selection.
Why It Matters
- CI time scales with change size, not codebase size - a 3-file change in a 500-module monorepo should take seconds to validate, not minutes; incremental builds make CI time proportional to the change, not the total codebase
- Agents benefit disproportionately - agents generate many small, focused changes; incremental builds turn each of those small changes into a fast CI run, dramatically increasing the iteration rate
- Enables monorepo CI to stay fast as codebases grow - full-rebuild CI degrades as codebases grow; incremental build CI stays fast because it only rebuilds the changed sub-graph
- Reduces compute cost proportionally - fewer compilation and test execution actions mean lower CI compute costs; for teams with many agent-generated commits per day, the cost savings are substantial
- Makes the "change small things often" pattern sustainable - incremental builds reward the small, focused commit style that AI agents naturally produce, reinforcing the pattern rather than punishing it
Getting Started
- Identify your build system's incremental build support - Gradle (Java/Kotlin): enable
--build-cacheandorg.gradle.incremental=true. TypeScript: usetsc --incrementalorts-node --transpile-onlyfor faster type checking. Rust: Cargo is incremental by default when CARGO_INCREMENTAL=1. Go: the build cache is incremental by default. JavaScript monorepos: consider Turborepo or Nx for package-level incremental builds. - Preserve the incremental build cache across CI runs - Incremental build state is stored on disk (
.gradle/,.tsbuildinfo,target/directory for Rust). Configure your CI cache to persist these directories between runs, keyed on the relevant source files. Without cache persistence, incremental builds restart from scratch every CI run and provide no benefit. - For monorepos: implement affected package detection - Turborepo's
--filter=[HEAD^1], Nx'saffected:test, and Pants'::with change detection all determine which packages are affected by a commit and run builds and tests only for those packages. Setting up these tools is a one-time investment that immediately benefits all subsequent CI runs. - Validate incremental build correctness - Incremental builds can have correctness bugs: a change that should trigger a downstream rebuild doesn't. Run the full build weekly (or on merge to main) to catch any incremental build correctness issues. Compare the incremental build results to the full build results and investigate any discrepancies.
- Configure your CI system to pass build state between jobs - If your CI pipeline splits into multiple jobs, each job needs access to the incremental build state. Pass build outputs between jobs using CI artifacts or a shared build cache. Don't split the build across jobs in a way that forces each job to recompile independently.
- Measure the before-and-after impact on CI time distribution - Track p50 and p95 CI time before and after enabling incremental builds. The p50 should drop significantly (typical changes benefit most). The p95 may not drop as much (changes to widely-depended-on modules still trigger large sub-graph builds). The distribution tells you where the remaining optimization opportunity is.
Turborepo's remote caching feature (available with Vercel or self-hosted) combines package-level incremental builds with a remote cache shared across developers and CI. This means when CI runs after a developer builds locally, it can hit the remote cache for packages the developer already built. The combination of incremental builds and remote caching is the JavaScript ecosystem's equivalent of Bazel.
Common Pitfalls
Incremental build false negatives. Not all build systems correctly detect all changes that should trigger a rebuild. If your build system uses file modification timestamps rather than content hashes to detect changes, fast file copies or git operations that don't change content can confuse it. Use content-hash based change detection (Bazel does this by default; Gradle supports it with --no-rebuild and correct task input declaration).
Cache invalidation bugs masking incremental build bugs. If your CI cache is misconfigured and always misses, incremental builds appear to work correctly because they always run from scratch - but you're not getting the speedup. Check whether your CI runs are actually restoring incremental build state by adding a log step that reports the size of the cache restore.
Applying incremental builds without correct dependency declarations. Build system tools infer dependencies between targets from explicit declarations (Bazel BUILD files, Turborepo turbo.json, Nx project.json). If a target has an implicit dependency that isn't declared, incremental builds won't rebuild it when its upstream dependency changes, producing incorrect results. Complete and accurate dependency declarations are required for correct incremental builds.
Over-relying on incremental builds and neglecting full builds. Incremental builds skip work by design. Over time, accumulated incremental build state can become stale or incorrect. Teams that never run full builds lose the safety net of a clean, from-scratch validation. Run full builds on merge to main and nightly on the main branch to catch anything that slips through incremental validation.
Implementing incremental builds in one language without others. In polyglot codebases, implementing Gradle incremental builds for Java while leaving Python builds as full rebuilds creates a two-speed CI. The total CI time is dominated by the slowest full-rebuild component. Address all languages in the build or the improvement is partial.
How Different Roles See It
Bob's team runs a Java monorepo with 15 services. CI is at 9 minutes, and the breakdown shows that 6 minutes is Java compilation. The team has already enabled Gradle build cache locally but not in CI. When Bob hears this, he realizes that the incremental build state being computed by developers locally is not being shared with CI - CI is doing full rebuilds while developers' local builds are already fast.
Bob should ask the CI owner to enable Gradle remote build cache with a simple self-hosted cache node (Gradle provides a Docker image for this). The CI configuration change is minimal - add --build-cache to the Gradle command and point to the remote cache URL. The expected result: CI compilation drops from 6 minutes to under 1 minute for typical branch builds that share compilation state with recent developer or CI runs. Bob should set a two-week measure period after the change and report the results to the team as a concrete example of infrastructure investment paying off quickly.
Sarah's CI timing analysis shows that 68% of CI runs for the JavaScript monorepo are building all 12 packages even when only 1-2 packages changed. She knows Nx has an affected command that would run builds and tests only for the packages affected by the current commit, but it hasn't been connected to CI.
Sarah should quantify the waste: if 68% of runs build 12 packages when they should build 2-3 packages, the team is doing 4-6x more work than necessary on most CI runs. At 9-minute CI, correctly scoped CI would take approximately 2-3 minutes for the typical change. Sarah should present this calculation to Bob alongside the Nx affected configuration - a two-day engineering task to implement - and frame it as "we are currently paying for and waiting for 6x more work than we need on every typical commit." The data makes the priority self-evident.
Victor has implemented Nx's affected command on his team's JavaScript monorepo and the results are clear: p50 CI time dropped from 7 minutes to 2 minutes after the change, because most agent-generated commits touch 1-2 packages in a 10-package repo. He's using Turborepo remote caching with Vercel for the remote artifact store. His agents now iterate at 30 cycles per hour instead of 8.
Victor should write up the configuration as a reference implementation: the turbo.json or nx.json configuration, the CI YAML changes, and the Vercel remote cache setup. He should include the before-and-after timing data and the agent iteration rate improvement. This reference implementation is what other JavaScript teams in the organization need to replicate the result. Victor should also investigate whether the pattern extends to the organization's Python services - Pants has equivalent affected-package detection and is worth evaluating for those teams.
Further Reading
From the Field
Recent releases, projects, and discussions relevant to this maturity level.
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.
CI/CD Pipeline