Your monorepo's CI runs the full test suite on every pull request, and it now takes forty minutes regardless of whether someone touched one file or a hundred. How would you fix that?
Build a dependency graph of the repository's build targets, diff the changed files against it to compute the affected subset, and run only that subset; the graph's correctness, not the diff itself, is what determines whether the speedup is trustworthy.
What the interviewer is scoring
- Does the candidate propose building a dependency graph rather than a heuristic based on file paths or directory conventions
- Can they explain concretely how a changed file maps to the set of targets that must be rebuilt or retested
- Whether they identify what happens when the graph itself is wrong, and how that failure mode differs from simply being slow
- Do they distinguish transitive dependents from direct dependencies when reasoning about what needs to rerun
- Do they mention verifying correctness with periodic full runs rather than trusting the affected-subset result permanently
Answer
Short answer
Build a dependency graph of the repository's build targets, diff the changed files against it to compute the affected subset, and run only that subset; the graph's correctness, not the diff itself, is what determines whether the speedup is trustworthy.
Running everything is a heuristic that just happens to be safe
Running the full suite on every change is not a design choice, it's the absence of one: it's correct by construction, because nothing that could possibly be affected is skipped, and its cost simply scales with the size of the whole repository regardless of the size of the change. That property is exactly what makes it wrong for a large monorepo. A one-line fix to a leaf module pays the same forty minutes as a change touching a dozen shared libraries, and the wait time has stopped correlating with the size of the actual risk.
The fix that actually reduces the number without reducing safety is to build a model of which targets depend on which other targets, and use that model to compute the specific subset of the repository that could possibly be affected by a given change. Everything else about the problem follows from how good that model is.
Building the graph, and where its accuracy actually comes from
A build graph, in this context, is a set of nodes — packages, modules, or build targets — and edges representing "depends on." Most build systems that support this (Bazel, Nx, Turborepo, and others each have their own name for it) construct the graph from declared dependencies: import statements, explicit dependency manifests, or a target's declared inputs. Given a set of changed files, you can map each file to the target that owns it, then walk the graph outward to every target that transitively depends on an owning target.
Change: a single file inside package `auth-core`
auth-core <- directly changed
^
|-- api-gateway (depends on auth-core)
| ^
| |-- checkout-service (depends on api-gateway)
|
|-- admin-portal (depends on auth-core)
Affected set: auth-core, api-gateway, checkout-service, admin-portal
Not affected: everything else in the repository, however large it is
The subtlety worth stating explicitly is that you need the transitive closure, not just direct dependents. checkout-service doesn't import auth-core at all — it depends on api-gateway, which does — but a change to auth-core can still break it, and stopping the walk at one hop silently drops it from the affected set.
Where affected-target detection actually goes wrong
The dangerous failure mode isn't slowness, it's a false negative: a target that was genuinely affected but wasn't included in the computed set, so it never runs and a regression ships untested. This happens whenever the graph doesn't capture a real dependency. A shared configuration file loaded at runtime rather than imported at build time, a database migration that several unrelated services rely on implicitly, or a build-tool version bump that changes compiled output for everything — none of these show up as an edge in a graph built purely from import statements, so a change to any of them computes an affected set that misses everything it should have included.
flowchart TD
A[Files changed in PR] --> B[Map files to<br/>owning targets]
B --> C[Walk transitive<br/>dependents in graph]
C --> D{Change type is<br/>import-graph visible}
D -- Yes --> E[Affected set is<br/>reliable]
D -- No --> F[Shared config, tool<br/>version, runtime dep]
F --> G[Graph misses it;<br/>false negative risk]The practical mitigations are to explicitly model the dependency types the import graph can't see — declaring that every target depends on the shared config file, even though no code imports it — and to treat certain classes of change, such as a build tool or base image bump, as always triggering a full run regardless of what the graph computes. Neither is a complete fix; both reduce the surface area of the blind spot.
Trusting the speedup without trusting it blindly
Because a false negative here is a correctness bug that ships silently rather than a build failure that's immediately visible, an affected-targets system needs a way to catch its own mistakes over time, not just on the day it's built. Running the full suite periodically — nightly, or on every merge to the main branch rather than on every PR — and treating any discrepancy between what the full run and the affected-subset run would have caught as a bug in the graph, is what keeps the model honest as the codebase and its real dependencies evolve.
The trade a team is actually making by adopting this isn't "faster CI for the same safety." It's faster CI for the same safety on changes the graph models correctly, and a periodic full run as insurance against the changes it doesn't. Stating that distinction is usually the difference between a candidate who has operated one of these systems and one who has only read about the speedup.
The forty-minute problem is solved by the graph, but the graph's blind spots are what determine whether the fast path is actually safe rather than merely fast.
© 2026 Preptima. Originally published at preptima.com.
Likely follow-ups
- How would you handle a change to a shared configuration file or a build tool version, which nothing depends on explicitly but which can break everything?
- What would make you distrust the affected-target result for a given PR and fall back to running everything?
- How would you test the graph itself for correctness, separately from testing the code it describes?
- How does this interact with a merge queue, where several PRs land close together and each recomputes an affected set against a slightly different base?
Related questions
- A popular key expires and forty thousand requests reach the database in the same second. What do you change?hardAlso on caching5 min
- A user's access to a document set is revoked. What has to happen across your RAG stack?hardAlso on caching6 min
- A document was updated an hour ago and the assistant is still quoting the old version. Walk me through the diagnosis.hardAlso on caching6 min
- How do you execute a global CDN cache invalidation for a critical security patch without melting your origin servers under a thundering herd?hardAlso on caching3 min