5.3.2 Practical Graph Analysis

Graph analysis becomes useful when it changes a concrete engineering decision: remove an accidental edge, split an overloaded target, stabilize a widely used API, or narrow the evidence needed for a performance investigation. The method is to state the graph contract, measure one bounded question, inspect the edges behind an outlier, and then verify the predicted effect with evidence from the graph layer where that effect occurs.

Establish the Measurement Contract

Start by recording five choices with every result:

  1. Layer: unconfigured targets (query), configured targets (cquery), or planned actions (aquery).
  2. Roots and universe: the top-level target for a forward walk, or the set of potential consumers for a reverse walk.
  3. Edge policy: whether implicit dependencies, tool dependencies, source files, generated files, and external repositories are included.
  4. Configuration: flags, platform, and top-level context whenever cquery or aquery is used.
  5. Comparison point: revision, Bazel version, and the same query expression when a metric is tracked over time.

Without that contract, two numbers called “closure size” may describe different graphs. query runs on the post-loading target graph and conservatively includes all possible select() branches. cquery runs after analysis and resolves them for the supplied configuration. aquery exposes the resulting actions and artifacts, but not which actions executed or hit a cache.1 This applies the graph-layer contract from 5.3.1 Graph Theory Foundations, builds on the target-graph model from 2.1.1 Nodes, Edges & Acyclicity, and uses the operators taught in 5.2.1 bazel query — Static Graph Analysis as diagnostic instruments rather than an API catalog.

For target-graph measurements, publish Bazel's edge direction:

dependent -> dependency
//app:server -> //lib:protocol

With that convention, fan-out is the number of outgoing edges from a target: its direct dependencies. Fan-in is the number of incoming edges: its direct dependents. The terms reverse if an exported data set reverses its edges, so publish the convention with the metric.

Map the metric to one operator

The expressions below are enough to follow this investigation. See 5.2.1 bazel query — Static Graph Analysis for the full semantics, universe rules, output choices, shell quoting, and failure modes.

MeasurementExpressionInterpretation limit
Forward closuredeps(root)includes the root. Not configured or executed work
Direct fan-outdeps(root, 1) except rootdirect dependencies in the chosen edge projection
Direct fan-inrdeps(universe, seed, 1) except seeddirect dependents only inside the chosen universe
Potential reverse impactrdeps(universe, seed)structural candidates, not guaranteed rebuilds
One explanationsomepath(from, to)one witness, not necessarily shortest or unique
Complete path subgraphallpaths(from, to)all target-graph paths in scope, not runtime flow

Choose implicit-edge filtering deliberately and keep it stable across comparisons. A source edit does not guarantee that every reverse-reachable target rebuilds or retests: configuration, action inputs, Skyframe pruning, produced bytes, and cache state determine observed work.2

Turn an Outlier into an Explanation

A high count is a triage result, not a repair. Inspect why the edges exist. Begin with one witness, then inspect the complete path subgraph before editing:

bazel query \
  'somepath(//services/orders:server, //lib:protocol)' \
  --noimplicit_deps \
  --output=label
bazel query \
  'allpaths(//services/orders:server, //lib:protocol)' \
  --noimplicit_deps \
  --output=label

Then inspect the suspicious target's loaded definition and provenance with --output=build, or locate the BUILD declaration with --output=location. Typical findings include a broad convenience library, a macro-added default, an obsolete direct dependency, or one target that groups unrelated sources. Build-file automation experience shows why the edge itself matters: missing dependencies can force broader compiler inputs, while unused dependencies add work and make downstream rules consume more than they need.3

The repair should match the evidence. Remove an unused edge, depend on a smaller public target, split a target only along real source/dependency boundaries, or make an intentional shared API explicit. After editing, rerun the same path and closure queries. A lower number is useful only if the unwanted paths disappeared and the build's semantics remain correct.

Visualize only when the path set needs a picture

For a human review, render the already-bounded allpaths() result:

bazel query \
  'allpaths(//services/orders:server, //lib:protocol)' \
  --noimplicit_deps \
  --output=graph \
  --nograph:factored | dot -Tsvg > /tmp/orders-protocol.svg

For exact automation, use a structured query format. DOT is presentation output. The factoring, quoting, and output caveats are covered in 5.2.1 bazel query — Static Graph Analysis.4

Treat Closure Counts as Trends and Leads

A closure trend is most useful as a change detector. Store the count with its measurement contract, compare like with like, and alert on a sustained or review-sized delta rather than a universal threshold. Then diff the sets to identify the newly reachable nodes:

bazel query 'deps(//app:server)' --noimplicit_deps --output=label \
  > /tmp/server-deps.txt

In durable automation, preserve structured output and compute the set diff in the consuming tool. The label file above is suitable for a quick human investigation, provided ordering is not treated as the result.

Interpret changes conservatively:

  • A growing forward closure is a lead for dependency bloat, licensing or ownership exposure, and loading/analysis investigation.
  • High fan-out is a lead for an over-broad consumer or aggregation target.
  • High fan-in is a lead for a shared API whose changes deserve compatibility care and a broad validation universe.
  • A path crossing an intended domain boundary is direct architecture evidence, even when the overall count is stable.

None of these observations automatically diagnoses poor cache performance or a large rebuild. Bazel exposes different evidence for the target, configured target, action, and observed execution layers.5 If select() or transitions can change the answer, repeat the closure under the actual build context:

bazel cquery \
  'deps(//app:server)' \
  --universe_scope=//app:server \
  --platforms=//platforms:linux \
  --noimplicit_deps

Read every result as a label-plus-configuration node. One label may appear in multiple configurations.6 If the hypothesis is “this structure registers too much work,” use aquery 'deps(...)' --output=summary to inspect planned action counts. If it is “this change caused more work or worse reuse,” compare matched profiles and execution logs. Query topology proposes the next measurement. It does not replace invocation evidence.

An Evidence-First Workflow

Use this sequence for a graph-health or dependency-bloat investigation:

  1. Write the observed problem and decision: unexpected dependency, risky API migration, analysis growth, or suspected action growth.
  2. Select the graph layer, roots or universe, configuration, and edge policy.
  3. Capture a baseline set and count. Retain the exact command and revision.
  4. Rank or shortlist candidates by direct fan-in, direct fan-out, or closure delta, without calling the metric a cause.
  5. Explain one candidate with somepath(), then use allpaths() before claiming an edge removal breaks reachability.
  6. Inspect the generating BUILD or macro definition and make the smallest semantics-preserving graph change.
  7. Rerun the same graph measurements and tests.
  8. Escalate to cquery, aquery, profiles, or execution logs to verify the configured, planned-action, rebuild, or cache claim that motivated the work.

This workflow produces an audit trail from symptom to graph evidence, from graph evidence to a bounded edit, and from the edit to the layer-specific outcome.

If this analysis will decide which CI work may be omitted, hand its graph evidence to the versioned, fail-closed service contract in 6.5.1 Affected-Target Service Contract. The commands here explain topology within a stated universe; they do not by themselves reconcile two revisions or turn unknown coverage into an unaffected result.

key takeaway

Define Bazel target edges as dependent-to-dependency: fan-out then means direct dependencies, while fan-in means direct dependents. Measure forward closure with deps() and reverse impact candidates with universe-scoped rdeps(), then explain outliers with paths and narrowly rendered DOT. Treat counts and trends as signals only. Verify configuration claims with cquery, planned work with aquery, and rebuild or cache claims with matched invocation evidence.

Check your understanding · 4 questions

1.rdeps(//services/payments/..., //lib:protocol) reports few consumers, but teams outside payments also use the library. What is the first issue to investigate?

Select one answer

2.somepath(A, B) returns one route. Before removing an edge and claiming that A can no longer reach B, what evidence is needed?

Select one answer

3.A repository-wide DOT rendering is unreadable, and a tool also needs exact graph data. Which responses follow the article's guardrails?

Select all that apply

4.Match each investigation claim to the evidence that can support it most directly:

Drag each answer onto the matching prompt, or click an answer and then click a prompt

Answers
Declared target reachability
Reachability under a selected configuration
Number and shape of planned actions
Actions that ran or reused cached results
0 of 4 answered

Footnotes

  1. The Bazel Query Reference — target-graph semantics and the boundaries between query, cquery, and aquery

  2. Query quickstart — quoted dependency queries, implicit-edge filtering, and the DOT-to-Graphviz workflow

  3. Automating Build Files — practical costs of missing, unused, and over-broad dependencies

  4. The Bazel Query Reference — reflexive closures, universe-scoped reverse dependencies, graph factoring, and structured output formats

  5. Extracting build performance metrics — separate evidence sources for target, configured-target, action, profile, and execution analysis

  6. Configurable Query (cquery) — configured-target identity, resolved select() branches, build options, and universe scope