The decision to consolidate services into a monorepo typically happens for good reasons: unified versioning, easier atomic refactors, reduced dependency matrix complexity across teams. But that same consolidation creates a dependency analysis problem that's qualitatively different from polyrepo analysis. In a polyrepo setup, a PR touches one service, and the question is: what else does that service affect? In a monorepo, a PR might touch a shared utility library imported by 40 downstream packages, and the question becomes: which of those 40 packages need re-testing, and which of those packages are actually deployed services versus internal-only build artifacts?
The monorepo dependency topology problem
A typical JavaScript or TypeScript monorepo organized with Nx, Turborepo, or Lerna contains a mix of node types that serve different roles:
- Deployed services — Express APIs, Next.js apps, queue workers. These have production blast radius.
- Shared libraries — utility functions, shared validators, design system components. These have no direct blast radius, but changes propagate to every service that imports them.
- Build-time tooling — ESLint configs, build scripts, test utilities. Changes here can affect CI behavior without affecting production runtime.
Treating all nodes the same in a monorepo dependency graph produces inflated blast radius scores. A change to a shared date formatting utility might technically have 38 dependents in the import graph, but if those 38 packages are 30 shared libraries and only 8 deployed services, the production blast radius is 8, not 38. Accurate monorepo analysis requires classifying nodes by deployment scope.
Package boundary detection at scale
In a well-structured monorepo using workspace-aware build tooling, package boundaries are explicit: each package has its own package.json (for Node.js), BUILD file (for Bazel), or project.json (for Nx). The dependency graph between packages is declared in these manifests.
The challenge is that monorepos are rarely perfectly structured. Internal packages import from each other in ways that bypass the declared manifest dependencies — path aliases, symlinks, shared type declarations via tsconfig paths. A scanner that only reads package manifests will miss these implicit edges. Complete monorepo dependency analysis requires parsing both the manifest declarations and the actual import statements in source files, then unifying the two graphs.
Consider a mid-size platform team running a TypeScript monorepo with 120 packages. Their @platform/auth-utils package has 14 declared consumers in package.json workspace references. But source scanning reveals 22 actual import paths, because eight packages import directly from ../../libs/auth-utils/src via TypeScript path aliases configured in the root tsconfig.json. The 8 undeclared consumers don't appear in the manifest graph. They would be invisible to a manifest-only scanner — and they'd be affected by any change to auth-utils.
Blast radius calculation in dense graphs
Monorepo graphs are dense compared to polyrepo service graphs. A shared utility library at the root of a monorepo might have transitive reach to nearly every deployable package in the repo. Naively computing blast radius by traversing all transitive dependents would flag almost every PR touching a shared package as HIGH risk, regardless of the actual change content.
More useful approaches weight blast radius by deployment scope and change type. A PR that changes a shared library's internal implementation without modifying its public API has lower effective blast radius than a PR that changes the function signature or removes an exported type. API surface change detection — comparing the before and after state of exported symbols — is an important input to monorepo blast radius scoring.
Bazel and Nx both have built-in affected-package computation (Nx's affected command, Bazel's target graph queries) that use the declared dependency graph to determine which downstream packages need rebuilding. These are good starting points, but they're build tools, not risk scoring tools — they tell you what's affected in the build, not what the deployment risk is if the change ships.
Graph analysis performance in large monorepos
A 500-package monorepo with dense import relationships can produce a dependency graph with tens of thousands of edges. Full graph traversal for every PR is computationally expensive. Two techniques keep analysis latency acceptable.
Incremental subgraph analysis. Rather than re-analyzing the full graph on every PR, only re-analyze the packages touched by the changed files plus their immediate neighbors. The persistent graph store provides the broader context; only the modified subgraph needs refresh. This reduces analysis time from O(total packages) to O(changed packages + direct neighbors).
Graph partitioning by deployment boundary. Separate the analysis into layers: first resolve which deployed services are in the transitive impact set, then run deeper analysis only on those services. Skip the intermediate shared-library nodes that have no independent production blast radius.
We're not saying monorepo dependency analysis is simple — it's genuinely harder than polyrepo analysis. We're saying the density of a well-managed monorepo is actually an asset for analysis: the explicit structure means fewer hidden dependencies than you'd find in the equivalent polyrepo setup, where service-to-service relationships often exist only in undocumented runtime calls.