Crux Read real Turborepo and Nx config snippets, predict the task graph and cache behaviour, and pick the highest-leverage fix.
Your altitude — climbing toward senior
ZeroJuniorMiddleSenior
You are at senior altitude — in orbit
◷ 14 min
The pipeline config and the cache key are where monorepo problems are actually diagnosed. Read the config, predict the task graph and what hashes into the cache, then choose the fix a senior engineer would make first.
Goal
Practise the loop you run on every slow or unsafe monorepo pipeline: read the task config, work out the ordering and the cache key, spot the false hit or false miss, and reach for the structural fix before adding hardware.
What ordering does this config produce, and what is the difference between the two dependsOn entries?
Heads-up The caret is load-bearing. ^build = the build task of this package's DEPENDENCIES; build = the build task of THIS package. Dropping the caret would skip building upstream libraries before the app that consumes them.
Heads-up ^build is scoped to the current package's own dependency subtree, not the whole repo. Turborepo walks the graph and only orders the upstream packages this one actually depends on.
Heads-up Empty outputs means there are no file artifacts to restore, but the task is still cached: a hit replays the recorded success and logs and skips re-running. outputs lists files to save, not whether caching happens.
A change to the shared root tsconfig.json alters compiler output, but every build still cache-hits and ships stale dist. Reading these inputs, what is the bug and the fix?
Heads-up outputs is correct — dist is the artifact to save and restore. The defect is on the INPUT side: an input that changes output (tsconfig) is missing from the hash, so the key never changes when it should.
Heads-up Ordering is fine for a build task; this is a stale-output false-hit problem, not an ordering problem. The fix is to widen the inputs to cover every file and tool that affects compiled output.
Heads-up That throws away the entire benefit of the cache to dodge one missing input. The senior fix is to make the key correct — add tsconfig and toolchain to the hash — so hits stay fast AND safe.
Snippet 3 — affected detection
# CI: only build/test what this PR could have brokennpx nx affected -t build test --base=origin/main --head=HEAD
Quiz
Completed
On a long-lived release branch this command suddenly reports the whole repo as affected for a tiny diff. Most likely cause?
Heads-up affected is deterministic: it is a graph walk over a git diff. A whole-repo result for a small change is a signal that the diff (the base and head range) is wrong, not that the algorithm is flaky.
Heads-up Affected scope is computed from the git diff and the project graph, entirely independent of cache fullness. A full cache causes misses (rebuilds), never an inflated affected set.
Heads-up Running multiple targets in one affected call is fine and intended. The inflated set comes from the base revision spanning too large a diff, not from combining targets.
Which import does this config reject at lint time, and what graph problem does the rejection prevent?
Heads-up That edge is explicitly ALLOWED: type:feature lists type:ui in onlyDependOnLibsWithTags. The config permits the downward direction feature → ui → util and rejects upward edges.
Heads-up enforce-module-boundaries fails lint on a violating dependency edge. It is a hard constraint on graph shape, not a style warning, which is what keeps the boundaries from eroding.
Heads-up util → util is permitted by the config, so that specific edge passes this rule. Direct cycles are caught separately; what this snippet rejects is the upward util → feature/ui direction.
Recap
Every monorepo pipeline problem is read in config and a git range: the caret in dependsOn decides whether upstream packages build first; the inputs glob is the cache key, where a missing tsconfig is a false hit and a volatile path is a false miss; affected scope is a graph walk over a diff, so a wrong —base inflates it to the whole repo; and depConstraints keep low-level libraries from gaining the fan-in that makes every change global. Read the config, fix the key or the boundary, then re-run and confirm the affected set and hit rate behave.