The build system decision record: why the monorepo toolchain you chose determines your incremental build cost and your cross-project dependency visibility

2026-07-04 · Decision record · Build systems · Monorepo · CI/CD

A 22-person TypeScript-first startup builds its product in a Yarn workspaces monorepo. In the first sprint, the founding engineer wires up Turborepo as the build orchestration layer: turbo run build test lint arranges the pipeline, turbo.json declares the task dependencies, and the remote cache is enabled with a Turborepo-provided token. At 12 packages, setup takes four hours and feels obviously correct. CI builds complete in under 3 minutes with a warm cache. The decision is never written down. There is no document that records why Turborepo over Nx, what the remote cache model requires at scale, or what the cost implications are when the free tier — a 10 GB team-level storage limit that evicts least-recently-used entries when full — encounters a monorepo that has grown past the cache's capacity.

Fourteen months after that first sprint, the platform has grown to 64 packages. The CI pipeline for a typical feature branch PR takes 31 minutes with a warm cache hit and 54 minutes on a cold cache. Cold cache builds occur more often than anticipated: on every feature branch after a merge conflict resolution that regenerates the lockfile, after the weekly automated dependency upgrade PR merges and changes the lockfile hash (invalidating cache entries for all 64 packages simultaneously), and on Tuesday mornings when the remote cache storage has filled over the weekend and LRU eviction has purged the output entries for the shared design system package — the most widely changed package, whose compiled outputs are declared as inputs to 37 downstream packages. When the design system cache entry is evicted, all 37 downstream packages must rebuild from scratch on the next CI run, cascading the cold build penalty across the majority of the pipeline. The weekly dependency upgrade alone costs approximately 8 CI minutes per affected engineer per week in lost cache warmth, multiplied across the 22-person engineering team.

The engineering team upgrades to Turborepo Remote Cache's paid plan at $400 per month to increase storage to 50 GB. The upgrade resolves the eviction problem for approximately four months, until the monorepo grows to 80 packages and the total cached output volume again approaches the storage ceiling. The cost is absorbed without revisiting the original decision, because the root cause — that the 10 GB free tier was sized for small monorepos and that its eviction behavior under a 64-package workload with weekly dependency churn was predictable from the cache model — had never been documented at selection time. An alternative build orchestration system (Nx) includes remote caching with no storage limit on its free community tier and provides a cost model that scales based on compute time rather than storage. The decision to use Turborepo was made when 12 packages fit comfortably in the free cache tier. The constraint embedded in that decision — that storage cost would become a variable as the monorepo grew — was discoverable at selection time but not documented, and was therefore not surfaced when it became active.

The second failure looks different but involves the same undocumented constraint. A 32-person fintech engineering team migrates six Java microservices from a polyrepo to a monorepo to improve shared library reuse and eliminate the per-service dependency version drift that had produced three production incidents over the previous year when different services consumed incompatible versions of the internal pricing library. They choose Gradle as the build system: the team is Java-first, Gradle is deeply familiar, and the existing per-service build.gradle configurations migrate without substantive rewriting. The Gradle build graph correctly models all Java-to-Java dependencies. Gradle's task output caching and the --build-cache flag provide incremental builds for all Java compilation and test targets. The migration completes in six weeks and the team considers it successful.

Over the following eighteen months, the monorepo expands to accommodate two Go services written by a newly hired systems engineering team (lower latency requirements for internal rate limiting and circuit-breaker APIs), a shared Protobuf schema project that defines the wire format for inter-service communication, and a Python-based ML pipeline for fraud signal generation. The Go services are built with Makefiles: the systems engineers are Go-native, the Makefile pattern is what they know, and nobody raises the question of how the Makefile build integrates with Gradle's dependency graph. The Protobuf schema project is a Gradle subproject that generates a Java SDK (consumed by the six Java services) and also runs protoc to produce Go source files, which the Go services' Makefile compiles into their binaries. The Go Makefile invokes make proto as a prerequisite target, which runs protoc directly rather than delegating to the Gradle Protobuf generation task.

When the Protobuf schema team adds a required currencyCode field to the PaymentRequest message, Gradle correctly invalidates the Java SDK build and rebuilds it, triggering downstream Java service rebuilds. The Go services' CI pipeline executes their Makefile, which calls make proto — but make proto checks whether the generated Go files already exist and, finding cached outputs from the previous successful build, skips the protoc invocation. The Makefile's change detection uses file modification timestamps, not content hashes, and the cached generated Go files have a newer modification timestamp than the .proto source files because the Gradle task that copies the .proto files into the shared directory preserves the original timestamp from the Git checkout. The Go services compile successfully against the stale generated types, all tests pass against test doubles that mock the payment service interface, and the CI pipeline reports green. The Go services ship to the staging environment with the old Protobuf schema.

The integration test environment discovers the schema mismatch 2 hours and 40 minutes into a staging validation run, when the rate limiting service attempts to parse a PaymentRequest from the updated Java payment service and encounters an unexpected required field. The failure manifests as a gRPC deserialization error deep in a test scenario for high-volume payment processing under load. The root cause investigation takes 90 minutes because there is no document that records that the Go services are built by Makefiles operating outside Gradle's dependency tracking, that the Protobuf schema project is a cross-language dependency with two separate generation paths operating under different change detection models, or that make proto's timestamp-based change detection produces incorrect cache hits when the upstream .proto files are touched by a Gradle task that preserves original timestamps. Nobody had documented that the build system's dependency graph had a gap at the Java-Go boundary, because that gap was not visible in the build tooling itself — it only became visible when a schema change crossed the boundary and found no mechanism to propagate.

Both teams made their build system decisions in the first week of a project, when the codebase was small enough that the choice felt like a configuration detail rather than an architectural constraint. The build system decision record is the document that makes the incremental build granularity, the remote cache model, and the dependency graph completeness requirement explicit — before the monorepo grows large enough for those constraints to become active costs.

What a build system decision record covers

Build system decisions are architectural decisions with compounding consequences: a build system chosen at 10 packages determines the CI cost structure at 100 packages, because migrating a large monorepo to a different build system requires rewriting build configurations for every package and rewriting every CI pipeline that invokes the build. The five decisions that belong in a build system ADR are:

Three structural properties that the build system choice decides

1. The incremental build boundary and the cost floor

The fundamental value proposition of a modern build orchestration system is incremental builds: only build targets whose inputs have changed, in the minimum necessary set. The boundary at which a build system detects a change determines what "inputs changed" means in practice, and the granularity of that boundary determines the cost floor — the irreducible CI build time that persists even with perfect remote cache hit rates on all unchanged targets.

File-granular build systems (Bazel, Pants) track individual source file hashes as the inputs to individual build targets. A change to one file invalidates only the specific targets that directly depend on that file, plus their transitive dependents. If the changed file is an internal implementation file whose exported interface is unchanged — a refactored private function, an updated comment block, an added internal helper — and no exported symbol changes, the dependency graph propagation stops at the package boundary: targets that depend on the package's exports are still cache-valid. File-granularity requires explicit BUILD file declarations for every target, which adds maintenance overhead, but delivers the narrowest possible rebuild scope.

Package-granular build systems (Turborepo, Nx) hash the contents of all files in a package directory to determine whether the package needs rebuilding. Any change to any file in the package — including documentation files, test fixtures, and internal implementation files with no exported changes — invalidates the package's cache entry. Every downstream package that declares a dependency on the changed package must also be rebuilt. A one-line comment fix in a widely-depended-upon utility package triggers a rebuild cascade across every package that lists it as a dependency, regardless of whether any exported behavior changed. Package-granularity is significantly easier to configure than file-granularity — no BUILD files required, just a package.json or equivalent — but it trades away the ability to scope rebuilds below the package level.

Script-based build invocations (npm workspace scripts, Makefiles without complete dependency graph declarations) have no build-system-level change detection beyond what the individual scripts implement. Each script runs to completion or skips based on whatever heuristic the script contains — typically file modification timestamps, the presence of output files, or no heuristic at all (always runs). Script-based builds are the default for projects that grow organically from small beginnings: a package.json with build and test scripts is the starting point for every Node.js project, and adding workspace-level orchestration via a root scripts/build-all.sh is a natural evolution. But script-based builds produce no machine-readable dependency graph and provide no native remote caching, so they do not provide the incremental build capability that makes monorepo-scale CI viable.

The cost floor at any given package count is a function of the chosen granularity, the dependency graph shape (a deep diamond graph with a widely-depended-upon base package produces larger rebuild cascades than a shallow tree), and the average task execution time per package. A package-granular system in a 100-package monorepo with a core utility package that 60 packages depend on produces a worst-case cold rebuild of 61 packages whenever the core utility changes. If the average test execution time is 90 seconds, the worst-case cold rebuild floor is approximately 91 minutes of sequential task time, which CI parallelism can reduce but not eliminate. A file-granular system for the same change — where only the exported interface changes invalidate dependents, and most commits do not change exported interfaces — can produce a worst-case rebuild of 1–5 targets per commit in many practical scenarios. The decision about build system granularity is a decision about the cost floor at future monorepo scale, made at a time when the monorepo is too small for the cost to be observable.

2. The dependency graph visibility surface and cross-project impact analysis

The dependency graph in a build system serves two distinct functions. The first is the operational function: the build system uses the graph to determine which targets need rebuilding when a file changes, to sequence task execution in the correct order, and to parallelize independent tasks across available CPU cores. The second is the observability function: the graph is a queryable map of the monorepo's dependency structure that engineers can interrogate to answer "if I change package X, what else will be affected?" before they make the change.

Build systems that require explicit dependency declarations — Bazel's BUILD files, Nx's project.json implicitDependencies, Gradle's implementation project(':subproject') declarations — create a machine-readable dependency graph as a first-class artifact of the build configuration. These graphs can be queried with build system tooling: bazel query 'rdeps(//..., //lib/core:lib)' returns all targets that transitively depend on //lib/core:lib, enabling pre-change impact analysis. Nx provides a visual dependency graph and a nx affected --target=build command that computes the set of projects affected by changes in the current branch against the base branch. Engineers can determine the blast radius of a proposed change before opening a PR, enabling better planning of large refactors that affect many downstream packages.

Build systems that infer dependencies from source analysis — TypeScript's paths and references compiler options, Python import analysis tools, Go module dependency analysis — have a dependency graph but it is produced by analysis tooling that runs separately from the build orchestration and may not be complete or up-to-date at the moment an engineer needs it. Source-inferred graphs are also language-specific: in a polyglot monorepo, the TypeScript dependency inference cannot detect that a TypeScript package implicitly depends on an output produced by a Python build step, because TypeScript analysis tools do not parse Python source. The cross-language dependency edges must be declared manually or remain invisible to the graph query tools.

The visibility surface — the set of dependency relationships that can be discovered by querying the build system — determines the engineering team's ability to reason about change impact in the monorepo. A team with a complete, queryable dependency graph can answer change impact questions at PR review time: "this PR modifies the shared auth middleware — Nx shows 14 affected projects, of which 3 have integration tests that must also pass." A team without a complete dependency graph can only answer this question by waiting for CI to run all affected tests and observing which ones fail, which converts a pre-change planning question into a post-change investigation.

The cross-language dependency gap is the most common source of dependency graph incompleteness in polyglot monorepos. When a new language is introduced — Go in a Java-primary monorepo, Python in a TypeScript-primary monorepo — the new language's build tooling is typically operated independently of the primary build system. The cross-language dependencies (a Go service consuming a Protobuf schema generated by a Java Gradle task, a Python ML pipeline consuming serialized model artifacts produced by a TypeScript training pipeline) are not modeled in either build system's graph, because each build system only models the targets it directly controls. The gap becomes visible only when a change to a shared artifact propagates through the primary build system's graph and is detected correctly for all targets in that language, but fails to propagate to the cross-language consumers whose dependency edge was never registered.

3. The remote cache hit rate ceiling and its eviction determinants

Remote caching is the mechanism that makes monorepo-scale CI tractable: instead of rebuilding all packages from scratch on every CI run, each build task checks a shared cache keyed by the hash of its inputs and reuses the cached output if available. A 90% remote cache hit rate on a 100-package monorepo with 5-minute average task time reduces CI time from 500 minutes of serial task time to 50 minutes of new-build tasks (plus parallelism gains). A 40% hit rate reduces CI time to 300 minutes, which at typical CI parallelism still translates to a 30–45-minute pipeline — 2–3x slower than the 90% case.

The remote cache hit rate is a function of three factors, each of which can independently limit the ceiling. The first is cache key design: the hash that identifies each build output must include exactly the inputs that affect the output and exclude inputs that do not. Including non-deterministic inputs — build timestamps, CI runner machine identifiers, absolute path prefixes, environment variables that vary across CI workers but do not affect the build output — produces different cache keys for identical builds, guaranteeing cache misses. Excluding relevant inputs — the version of the compiler, the contents of indirect dependencies that are not modeled as explicit graph edges, environment variables that do affect build behavior like NODE_ENV or GOFLAGS — produces incorrect cache hits where a stale output is served for a changed input. Both failure modes are subtle and produce inconsistent CI behavior rather than reproducible failures. Cache key design requires deliberately auditing what inputs affect each task's output and ensuring that exactly those inputs are included in the key, no more and no less.

The second factor is cache storage capacity. A remote cache that fills and begins evicting entries before they are reused produces a hit rate that is structurally limited by the eviction frequency. The eviction policy — LRU, LFU, FIFO, or TTL-based — determines which entries are removed under storage pressure. LRU eviction in a monorepo where all packages are rebuilt together on dependency upgrades will evict the oldest entries — typically the test outputs for packages that are stable and rarely change. Those entries are also the most valuable cache entries, because they represent packages that never need to be rebuilt. After a lockfile upgrade invalidates all entries simultaneously, the cache fills with fresh entries for every package. The large shared packages fill the most cache space. As new PRs add more entries, the LRU policy evicts the oldest entries — which are the previously stable packages whose cache entries were valid before the lockfile upgrade. Those packages are then rebuilt on the next cache-cold PR, and the cycle continues.

The third factor is the frequency of global cache invalidation events — changes that invalidate all cache entries regardless of individual target inputs. Lockfile changes invalidate all packages in systems where the lockfile hash is included in the cache key (it should be, because a dependency version change can affect any package). Compiler version upgrades invalidate all compiled outputs. CI environment changes (base image updates, tool version bumps in the CI configuration) invalidate all entries if those environment attributes are included in the cache key. A team with weekly automated dependency upgrades (Renovate or Dependabot in aggressive mode) experiences approximately 52 global cache invalidation events per year — once per week, on the Monday morning the dependency upgrade PR merges. If each global invalidation forces a 100% cold build on the next two or three PRs before the cache warms back up, the dependency upgrade cadence alone sets a ceiling on the achievable average cache hit rate. A weekly upgrade cadence in a 100-package monorepo with 5-minute average build time produces approximately 52 full cold rebuilds per year — 52 × 500 minutes = 430 hours of CI time that cannot be cached away, regardless of cache storage capacity. This is a predictable cost that can be modeled at build system selection time and compared against the alternative of a monthly or quarterly dependency upgrade cadence.

Five ADR sections for a build system decision record

1. Build system selection and polyglot requirements

Document the primary build orchestration tool chosen and the alternatives considered, with explicit reasoning for each rejection. For a TypeScript-primary monorepo, the realistic choice set in 2024–2026 is Turborepo (fastest setup, limited graph querying, storage-bounded remote cache in the free tier), Nx (richer project graph tooling, workspace generators, no-storage-limit free remote cache, higher initial configuration overhead), or Bazel (file-granular change detection, deterministic builds, steep learning curve, requires BUILD file maintenance for every target). The reasoning should state which characteristics were weighted most heavily in the selection — setup speed for an early-stage team, CI cost for a cost-constrained team, polyglot support for a team with planned language diversity — because those weights may shift over the product's lifecycle in ways that would warrant re-evaluating the choice.

Document the anticipated polyglot surface explicitly: what languages and artifact types does the current monorepo contain, and what additions are planned or plausible in the next 12 months? If the primary build system has no native support for a planned addition — Turborepo orchestrates tasks defined in package.json scripts and has no native Protobuf or Go support beyond wrapping shell commands — document how that addition will integrate with the primary build system's dependency graph. The options are: (a) the secondary language's build is wrapped as a shell script task in the primary build system, with inputs and outputs explicitly declared so the primary system can model the task in its graph; (b) the secondary language uses its own build tooling with no integration into the primary graph, accepting the cross-language dependency visibility gap; or (c) the team adopts a polyglot build system (Bazel, Pants) that natively models all languages in a single graph. Option (b) should not be chosen by default because it is the default — it should be an explicit decision with documented awareness of the cross-language dependency gap it creates.

Document the migration cost model for the chosen build system. What is the effort required to migrate from the chosen build system to an alternative if the monorepo grows 10× and the current choice's cost characteristics become unacceptable? Migrating a 100-package Turborepo monorepo to Nx requires rewriting build configuration for every package and validating that cache key equivalence is preserved; the effort is proportional to package count. Migrating from Nx to Bazel requires writing BUILD files for every target and replacing all CI invocations; the effort is proportional to both package count and target count within packages. A team that chooses Turborepo at 10 packages knowing that the migration cost to Nx at 100 packages is approximately 2–3 engineering weeks is making an informed trade-off. A team that makes the same choice without modeling the migration cost discovers the trade-off when they are facing it, with a larger migration surface than they anticipated.

2. Incremental build strategy and change detection granularity

Document the change detection granularity explicitly — file-level, package-level, or script-invocation-level — and record what the implications are for rebuild scope as the monorepo grows. For package-granular systems, document the expected maximum dependency fan-out: which packages, if any, are depended on by a large number of other packages, and what the rebuild cascade looks like if those packages change frequently. The design system package in a product monorepo, the shared authentication middleware in a services monorepo, and the core utility library in any monorepo are typically the packages with the highest fan-out. If the team anticipates frequent changes to high-fan-out packages, the cost of package-granular change detection at the chosen fan-out should be modeled.

Document the task dependency graph structure explicitly: which tasks must run before which other tasks, what task outputs are declared as inputs to downstream tasks, and what the critical path is through the dependency graph for the most common build scenario (typically: build → test → lint for each package, with the build outputs of depended-upon packages declared as inputs to dependent package builds). An undocumented task dependency means that the build system cannot sequence execution correctly, potentially producing nondeterministic builds where a task runs before its upstream dependencies are complete. Document which tasks can run in parallel across packages (test tasks for independent packages can parallelize) and which cannot (a package's test task cannot run until that package's build task completes, and cannot run until all upstream dependency build tasks complete).

Document the affected-package detection mechanism for CI: the algorithm by which the CI pipeline determines which packages are affected by the changes in a given PR and must have their tasks run, as opposed to which packages can safely use their cached outputs. For Turborepo, this is determined by the task output cache: a package whose input hash matches a cached entry uses that entry and marks its tasks as complete without executing them. For Nx, the nx affected command computes the affected project set by comparing the current branch to a base revision and traversing the dependency graph to find all projects transitively downstream of the changed projects. Document the base revision that nx affected compares against — typically the merge base with the default branch — and what happens when the base revision cannot be computed (a force-pushed branch, a shallow clone that does not include the merge base commit). Undefined behavior in affected-package detection produces either over-building (every task runs, cache is bypassed) or under-building (some affected tasks are skipped, producing incorrect CI results); both failure modes should be documented with their mitigations.

3. Remote cache architecture and eviction policy

Document the remote cache provider, the cache key design, the storage model, and the expected cache hit rate at current and projected monorepo scale. The cache key design should enumerate every input that is hashed into the cache key for each task type. For a Turborepo TypeScript build task, the typical cache key inputs are: the hash of all source files in the package, the hash of the package.json (including version), the hash of the root turbo.json (build pipeline configuration), the hash of the lockfile (ensuring all dependencies are at the declared versions), and the hash of any declared globalEnv environment variables. Document which CI environment variables are included in the cache key and why — adding an environment variable to the cache key reduces false cache hits at the cost of reducing cache hit rate when the variable changes between CI runs. Document which variables are explicitly excluded and confirm that they genuinely do not affect build outputs.

Document the storage capacity model explicitly. For Turborepo Remote Cache, the free tier provides 10 GB of storage with LRU eviction; the paid tiers provide larger storage. For Nx Cloud, the free tier provides no-storage-limit caching for open-source and small commercial teams. For self-hosted cache providers (Turborepo's open-source turborepo-remote-cache server, Nx Cloud's self-hosted option, or a custom S3-backed cache), the storage is bounded by the provider's infrastructure costs. Calculate the expected cache storage requirement for the monorepo at its current and projected scale: estimate the average compressed output size per package (typically 10–100 MB for compiled TypeScript with source maps and test result artifacts), multiply by the number of packages, multiply by the number of distinct cache entries that need to be retained (typically 5–10 per package, corresponding to the last few PRs' cached outputs), and compare against the available storage. If the expected storage requirement exceeds the available storage within the planning horizon, document the upgrade path and its cost before that threshold is reached.

Document the cache eviction behavior under storage pressure and its expected impact on cache hit rates. For LRU eviction, identify which packages are most likely to be evicted first (infrequently changed, low fan-in packages that are built rarely and whose cache entries are oldest when storage fills) and assess whether those packages are cheap or expensive to rebuild. Document the expected frequency of global cache invalidation events (lockfile upgrades, compiler version bumps, CI environment changes) and their interaction with the cache storage model. A weekly lockfile upgrade cadence that triggers a 100% cold build in a 64-package monorepo produces a predictable cache miss rate floor that cannot be reduced by increasing storage capacity — it can only be reduced by changing the lockfile upgrade cadence or by designing the cache key to not include the full lockfile hash (at the cost of occasional incorrect cache hits when a transitive dependency update affects a package's runtime behavior without changing its TypeScript types).

4. Dependency graph enforcement and cross-project visibility

Document the mechanism by which inter-package dependencies must be declared and the tooling by which the completeness of those declarations is validated. For TypeScript monorepos using TypeScript Project References, the tsconfig.json references array must be kept in sync with the actual import relationships between packages — TypeScript's compiler enforces that a package can only import from packages listed in its references, but this enforcement is opt-in (it requires composite: true and incremental: true in the referenced packages' configurations). Document whether TypeScript Project References enforcement is enabled for all packages, and what the CI check is that prevents a package from importing from a package it has not declared as a reference. For Nx monorepos, the @nrwl/enforce-module-boundaries ESLint rule enforces that packages only import from packages whose import path matches the declared dependency, and can be configured to fail CI on undeclared imports. Document what enforcement is in place and what the gap is between the declared dependency graph and the actual import relationships.

Document the cross-language dependency gap explicitly for every language pair in the monorepo that shares a build artifact. For each cross-language dependency (a Go service consuming Protobuf-generated Go types produced by the primary build system's Protobuf task, a Python service consuming a shared configuration schema produced by a TypeScript build), document: which build system is responsible for producing the upstream artifact, which build system is responsible for consuming it, how the upstream build system notifies the downstream build system that the artifact has changed, and what happens if the notification fails (the downstream build proceeds with a stale artifact). If there is no notification mechanism — if the cross-language dependency is implicit, bridged only by a shared directory path — document that gap and the engineering procedure for ensuring the dependency is respected: for example, "the Go Makefile must be run after the Gradle Protobuf generation task whenever a .proto file changes — this is enforced by the CI pipeline job ordering (the Go build job lists the Gradle Protobuf job as a needs dependency in .github/workflows/ci.yml), not by either build system's dependency graph."

Document the tooling available for querying the dependency graph before a change is merged. For Nx, document the nx graph command and the nx affected --target=build --dry-run command that previews which projects would be affected by the current branch's changes. For Bazel, document the bazel query syntax for finding all targets that depend on a given target. For Turborepo, document the limitations — Turborepo's turbo run --dry-run shows which tasks will execute based on cache state but does not provide a static dependency graph query tool for impact analysis. If the chosen build system lacks native dependency graph query tooling, document the alternative — Nx's project graph visualization, a custom script that parses package.json dependencies, or a manual review process — and its limitations.

5. CI integration, parallelization strategy, and affected-package detection

Document the CI pipeline topology: how build tasks are distributed across CI workers, how the affected-package detection output drives which tasks are scheduled, and how the CI pipeline fails fast when a task fails without waiting for unrelated tasks to complete. For a Turborepo-based pipeline, the typical topology runs turbo run build test lint on a single CI worker (the orchestration is internal to Turborepo, which parallelizes tasks within the worker's available cores), with the remote cache providing the incremental build capability. An alternative topology distributes packages across multiple CI workers using a matrix strategy and runs each package's tasks independently — this provides better parallelism at the cost of higher CI worker count and more complex pipeline coordination. Document which topology is chosen and what the expected CI time is at current and projected package count for each topology.

Document the affected-package detection strategy and its integration with the CI pipeline. For a PR-based workflow, the affected detection typically compares the PR branch against the default branch's merge base and runs tasks only for the affected packages. Document what "affected" means precisely: a package is directly affected if any of its source files change; a package is transitively affected if any of its declared dependencies are directly or transitively affected. Document the behavior when the detection algorithm cannot be applied — for example, when the merge base is not available in a shallow clone, when the PR is targeting a non-default branch, or when a change to a global configuration file (a root tsconfig.json, a root jest.config.js) should transitively affect all packages but the build system does not model that dependency. For these cases, document the fallback: either run all tasks (safe but expensive) or skip affected detection and run only the tasks for packages that directly changed (fast but may miss transitive effects of global configuration changes).

Document the CI build time SLA and the alert threshold. Establish an acceptable maximum CI build time for the monorepo at current scale (typically 15–20 minutes for interactive feedback during a PR review cycle) and the process triggered when CI builds consistently exceed that threshold. The process should include: identifying which tasks are on the critical path (whose sequential execution time determines the overall build time), whether the critical path can be shortened by increasing task parallelism, whether affected-package detection is accurately scoping the task set, whether the remote cache hit rate is at the expected level, and whether a build system migration to a finer-grained alternative is warranted. Document the scale threshold at which a migration evaluation is automatically triggered: for example, "if the average warm-cache CI build time for a standard PR exceeds 20 minutes for two consecutive weeks, an ADR review is triggered to evaluate whether the current build system selection remains appropriate at the current monorepo scale." Without this trigger, build time creep is absorbed gradually until it becomes a significant engineering productivity cost with no documented evaluation point at which the original decision was revisited.

Further reading