Cut Monorepo CI Time with Affected Builds and Dependency Graphs

A practical method for making monorepo affected builds fast without trusting an incomplete graph, weak cache keys, or a change detector that silently skips required work.

Edilec Research Updated 2026-07-13 Cloud & DevOps

Monorepo affected builds replace the blunt rule of running every task after every change with a narrower claim: run every task whose result could differ, and reuse a trustworthy result for everything else. That claim is valuable only when the repository's dependency graph, changed-file comparison, task inputs, and cache identity are all correct. A pipeline that saves ten minutes but occasionally misses a generated client, integration test, policy check, or deployment bundle has not been optimized. It has exchanged visible compute cost for hidden release risk.

The right objective is therefore not the smallest possible task set. It is the smallest defensible task set, backed by evidence and periodic full validation. Nx affected combines Git's changed files with a project graph and traverses dependants; the same underlying reasoning can be implemented with other graph-aware systems. The design work lies in making the graph describe reality, defining what globally invalidates it, and measuring false negatives rather than celebrating a low number of selected jobs.

Define the affected-build correctness contract

Write the contract before changing CI. For a base revision B and head H, the selector must identify every task whose declared or implicit input differs, every downstream task that consumes a changed output, and every task invalidated by a global input. Inputs include source, lockfiles, compiler and test configuration, environment variables that affect output, code generators, toolchains, container bases, and policy bundles. The contract should also say what happens when the base revision is unavailable, graph construction fails, a plugin cannot parse a project, or the selector encounters an unknown file. A conservative full run is a valid answer; silently returning an empty set is not.

Six-stage path from changed files and comparison base through dependency graph traversal, global invalidation, task selection, cache verification, and shadow full-run validation
Affected execution is safe when graph and cache decisions remain explainable and are continuously checked against complete runs.
Change classGraph treatmentTypical selected workFallback
Leaf implementationMark owning project and reverse dependantsLeaf tests plus consumer builds or testsRun owning domain
Public interfaceTraverse all consumersContract tests, consumers, deployablesRun broad integration tier
Lockfile or toolchainTreat as global or map package consumersAll tasks using changed resolutionRun all if mapping is uncertain
CI or policy fileUse explicit named inputsAffected policy and delivery tasksRun complete validation
Unknown pathFail closedDiagnostic plus safe task setRun all

Build a trustworthy project and task graph

Nx dependency examples showing that an application change affects one application while a shared-library change affects the library and both dependent applications
Dependency direction determines the affected set: an application-only change stays local, while a shared-library change reaches every application that depends on it.

A project graph says which logical units depend on which others; a task graph adds target-specific ordering such as build before test or schema generation before compilation. Keep both machine-derived where possible. Static imports, package manifests, build declarations, generated-code ownership, API schemas, and deployment composition are stronger evidence than a hand-maintained list. Bazel's query guide demonstrates useful graph interrogation with deps, reverse dependencies, somepath, and allpaths. Equivalent queries should be available to developers investigating why a task was or was not selected.

Represent edges that the compiler cannot see. A service may consume an OpenAPI specification through generated code, a dashboard may depend on an event schema without importing its producer, and an end-to-end suite may exercise several deployables. Add explicit metadata for these relationships and assign an owner to every exception. Validate boundaries in CI so a new cross-project import cannot bypass graph registration. Graph freshness is an operating metric: track unowned projects, unresolved imports, cycles, projects with suspiciously no dependants, and changes that select almost everything.

Choose the comparison range safely

A correct graph can still produce the wrong set when the comparison base is wrong. Pull requests normally compare the merge base or the last successful mainline revision with the candidate head. Mainline runs should cover everything since the last successful run, not merely the latest commit, because a failed or cancelled predecessor leaves unvalidated changes behind. Fetch sufficient history and record both SHAs in the build evidence. On rebases, merge queues, and stacked changes, make the CI event's semantics explicit. When the selected base cannot be resolved, emit a diagnostic and execute the documented full-run fallback.

Separate task selection from cache reuse

Affected analysis answers which tasks may have different results. Caching asks whether an identical task with identical inputs has already completed. These are independent controls and should be observable independently. A selected task may be a cache hit; an unaffected task need not be scheduled. Nx remote cache shares task results across machines, while GitHub's dependency caching reference explains cache matching through explicit keys and restore keys. Dependency caches accelerate setup, but they are not proof that a build output is valid.

InputWhy it changes outputGood key materialCommon mistake
Source closureTask reads project and dependency filesContent digests from declared inputsHashing only the project directory
Dependency resolutionResolved versions alter compilation or testsLockfile or resolved graph digestUsing manifest without lockfile
ToolchainCompiler and runtime behavior changesPinned image and tool versionsKeying on a mutable image tag
Task configurationFlags and environment shape outputsNormalized command and named variablesIgnoring CI-only flags
PlatformNative output can vary by OS or architectureOS, architecture, ABI where relevantSharing native cache across platforms

Validate the selector with shadow and full runs

Treat the selector as production software. For an initial period, calculate the affected set but still run the full suite, then compare outcomes. A false negative is any task excluded by the selector that fails or produces a different output in the full run. Investigate false positives too because they reveal coarse boundaries or global inputs that erase savings. After launch, retain scheduled full runs, full validation on release branches, and a manual full-run control. Sample ordinary pull requests for shadow execution so graph drift is detected before the next nightly window.

Store selection evidence with each run: changed files, base and head, graph version, selected projects, selected tasks, invalidation reason, cache hit status, and fallback state. This evidence turns a suspected miss from guesswork into an explainable chain. Alert when graph construction errors rise, selection unexpectedly drops to zero, a familiar change class selects far fewer tasks than its baseline, or the ratio of full-run failures to affected-run failures changes. The core reliability indicator is escaped defect or divergent-result rate, not median task count.

Roll out affected execution in controlled steps

Start with deterministic, well-bounded tasks such as linting and unit tests for leaf projects. Establish graph diagnostics and cache observability, then add builds, consumer tests, generated artifacts, and integration suites. Keep a global-input registry in version control and require review from build engineering when it changes. For each task class, define its input closure, graph edges, safe fallback, full-run cadence, and owner. Move a class from shadow to enforcing mode only after representative changes show no unexplained misses and the team can reproduce selection locally.

Use latency distributions to decide where to invest next. If most pull requests touch a central shared library and legitimately select most of the graph, more runners will mask an architectural problem but not remove it. Split overly broad libraries, stabilize public interfaces, or create narrower test contracts. If selected tasks are few but execution remains slow, improve task parallelism or cacheability. If time is spent waiting for a runner, fix scheduling and capacity separately. Affected builds work best as one layer in a CI system whose bottlenecks are measured rather than assumed.

Assign graph stewardship without centralizing every edge change. Project owners maintain declarations near their code, while build engineering owns schema, validation, selector releases, and emergency fallback. Add a pull-request explanation that lists why each expensive task is selected and the dependency path responsible. Developers can then repair accidental coupling instead of learning to distrust the selector. Review top invalidators monthly: a global file changed frequently, an integration suite attached to every project, or an oversized shared package often offers more durable savings than another selector heuristic.

Affected builds key takeaways

  • Define selection as a correctness contract, including global inputs and fail-closed behavior.
  • Derive project and task graphs from build evidence, then model implicit schema and integration edges explicitly.
  • Compare against the last successfully validated state and preserve enough Git history to resolve it.
  • Keep affected selection and cache reuse as separate, observable decisions.
  • Measure false negatives through shadow runs, scheduled full runs, and output comparison.
  • Use broad affected sets as feedback about repository boundaries, not as a reason to weaken safety.

Monorepo affected builds FAQ

Should a lockfile change affect every project? That is the safest default. A package-manager-aware mapping can narrow the set only when it reliably identifies which resolved dependencies changed and every project that consumes them. Preserve the full-run fallback for unsupported or ambiguous changes.

Do affected builds replace a remote cache? No. Selection avoids scheduling tasks that cannot have changed; a remote build cache avoids recomputing a selected task whose complete input identity already has a trusted result. Both depend on accurate inputs, but they solve different waste.

How often should the complete suite run? Match cadence to change rate and consequence. Many teams run it nightly and for releases, plus sampled pull requests. High-risk repositories may run broader suites on every merge. The important control is bounded time to discover a selector miss.

Conclusion

Fast monorepo CI comes from making less work necessary and making repeated work reusable, without weakening the evidence behind a green build. A complete dependency model, explicit global inputs, correct comparison range, content-based cache identity, and continuous full-run validation make affected execution defensible. Optimize the graph as a maintained product, and shorter pipelines can increase both delivery speed and confidence.

Continue with related articles