5.3.1 Graph Theory Foundations
recommendedWhen one dependency seems to join two teams, a target sits on every visible route, or a closure keeps growing, the useful question is not merely “how big is the graph?” You need to know whether the structure suggests an architecture boundary, a choke point, an ordering constraint, or a broad reachability set.
2.1.1 Nodes, Edges & Acyclicity established the basic target DAG. At consultant depth, a graph finding is useful only after you name the graph: the same Bazel label can participate in the unconfigured target graph, become several configured-target nodes, and register zero or more actions. Connected components, bridges, topological order, and transitive closure answer different questions on each layer. Choose the layer and scope before interpreting the result.
Fix the Graph Contract First
For the examples below, write a target edge as
//app:server -> //lib:protocol
meaning “//app:server depends on //lib:protocol.” This is the direction used
by Bazel query graphs: an edge points from a target to one of its prerequisites.
deps(//app:server) therefore walks with the arrows, while reverse-dependency
analysis walks against them.1
The same small graph makes the four structural questions easier to keep separate:
Which targets can server reach by following arrows?
Answer: server, facade, model, auth, and types — including the seed itself.
Which groups stay connected if arrows become lines?
Which one vertex or edge would split a component?
Which listing puts every dependent before its dependency?
server · lint · facade · rules · model · auth · types
That target graph is the right default for questions about declared architecture. It is not the only Bazel graph:
| Question | Node identity | Evidence |
|---|---|---|
| What can the BUILD declarations depend on? | unconfigured target | query |
| What does this build configuration actually select? | label plus configuration | cquery |
| What compile, link, or generation work is planned? | action and artifact | aquery |
| What ran, waited, or hit a cache in one invocation? | observed execution event | profile, execution log, or BEP |
query runs after loading and conservatively includes all possible select()
branches. cquery runs after analysis, resolves those branches, and can contain
more than one configured node for the same label.2 aquery exposes the
actions and artifacts produced by analysis. It does not report what executed or
whether an action was a cache hit. 2.1.5 Inspecting the Graph — Query Preview introduces this layer
choice, while 5.2.1 bazel query — Static Graph Analysis, 5.2.2 bazel cquery — Configured Graph, and 5.2.3 bazel aquery — Action Graph
provide the operating details.
Also declare the scope and projection. Results change when you include source file targets, implicit dependencies, external repositories, or only one configured universe. For classic connected-component and bridge algorithms, you must additionally say that direction is being ignored. Without those choices, “the graph has three components” is not a reproducible finding.
Connected Components: Candidate Boundaries
In an undirected graph, a connected component is a maximal set of nodes joined by paths. For a Bazel dependency graph, the useful analogue is usually a weakly connected component: take the selected directed graph, temporarily ignore edge direction, then compute its components.3 Strongly connected components ask a different question—mutual directed reachability—and are not a useful decomposition of an acyclic configured build graph.
Suppose //payments/... and //search/... fall into different weak components
after you restrict the graph to first-party rule targets. That is evidence that
neither area has a target-dependency path to the other under this projection.
It makes them candidates for separate ownership, extraction, or independently
scoped validation. It is not yet proof that they are operationally independent:
including a shared generated target, resolved toolchain, external repository,
or configuration-specific edge may connect them on another layer.
Nor does a component count predict runtime parallelism. Two disconnected target subgraphs can still compete for CPU, memory, workers, or remote capacity. A single connected component can expose abundant action-level concurrency. Use components to propose architecture boundaries. Use the analyzed action graph and a profile to evaluate scheduling and elapsed time.
When exporting query results for an algorithm, preserve node identity and
edges. The default Graphviz output may factor topologically equivalent nodes,
which is helpful for pictures but unsuitable as an exact algorithm input.
disable factoring or consume a structured output that lets the analysis tool
reconstruct the graph.1
Articulation Points and Bridges: Structural Choke Points
On the same undirected projection, an articulation point is a vertex whose removal increases the number of connected components. A bridge is an edge whose removal does so.3 They answer: “Which single vertex or dependency link holds these regions together?”
That is not the same as fan-in or change impact:
- A bridge can connect two small regions and have few reverse dependencies.
- A heavily used foundation can have high fan-in without being an articulation point because alternative undirected paths keep the graph connected.
- Removing a target is a hypothetical graph operation. Editing its sources does not remove the node or its edges.
Use a bridge finding to inspect a cross-domain dependency: should the consumer
depend on a smaller public interface, should shared code move to a neutral
package, or is this an intentional boundary that needs explicit ownership and
compatibility policy? Use an articulation finding to locate a target that mixes
otherwise separate domains and may deserve decomposition. Then verify the
specific directed relationship. If //app:server must stop reaching
//legacy:api, inspect allpaths(//app:server, //legacy:api): breaking one
apparent edge is insufficient when another directed path remains.4
If the real question is “what could be affected by changing this library?”,
compute reverse reachability with rdeps(universe, library) instead. That
result is a structural candidate set, not a promise that all members rebuild or
retest. Configuration, action inputs, pruning, and cache state decide what work
an invocation actually performs.
Topological Order: Constraints, Not a Schedule
A topological order linearizes a DAG while respecting its directed edges, but
many valid orders may exist. The direction convention matters. Because a Bazel
query edge points from a dependent to its prerequisite,
--order_output=deps prints dependents first and dependencies afterward.1
This is a valid topological order for that edge orientation. It is the reverse
of a “build-ready” listing that puts prerequisites before their consumers.
Do not read ordinary lexical output as graph order: current query defaults can
print labels lexicographically, and nodes unrelated by reachability have no
dependency-imposed order.1 Even a dependency-ordered target list is not
Bazel's execution schedule. Targets are not actions, and the target-to-action
mapping is not one-to-one. Resource limits, execution strategies, cache reuse,
and action durations all affect the observed schedule.
Topological reasoning is still useful for finding constraints. A long chain in the target graph suggests architectural layering to inspect. A long chain in the action DAG can constrain execution. To make a performance decision, examine the latter with action-graph and profile evidence. A target ordering alone cannot show how much parallelism was available or used.5
Transitive Closure: Reachability with a Direction
The forward transitive closure of a target is everything reachable by following
dependency edges. Bazel's deps(x) computes the reflexive transitive closure,
so the result includes x itself. Its optional depth bounds the traversal.
rdeps(u, x) computes reverse reachability from x, but only within the
transitive closure of the chosen universe u.1
These two closures support different decisions:
deps(service)describes the declared dependency footprint to review for unexpected libraries, policy violations, licensing exposure, or a proposed extraction.rdeps(protected_universe, library)describes the consumers that may need migration, compatibility review, or targeted validation when an API changes.cquery deps(service)asks the forward question for a particular configured context, whereselect()and transitions matter.2
Closure size is a signal, not a diagnosis. A growing target closure may justify
investigating dependency bloat, but it does not by itself measure configured
targets, registered actions, action inputs, cache keys, cache hits, or executed
work. A large closure can be mostly reusable. A small closure can contain one
expensive action. Investigate suspected analysis breadth with configured-target
counts and profiles, and inspect planned work with action counts. Confirm rebuild
or cache behavior with profiles and execution logs—not with wc -l over query
output.5
Finally, keep the Bzlmod module graph separate. bazel mod graph explains
external module resolution. It does not replace query for target
dependencies, cquery for configured targets, or aquery for actions.6
Apply graph theory only after declaring the Bazel layer, node identity, edge
direction, universe, and projection. Weak components suggest architecture
boundaries. Articulation points and bridges identify single structural
connections, not high-impact changes. Topological order expresses dependency
constraints, not an execution schedule, and transitive closure measures
reachability, not rebuilds or cache effectiveness. Escalate from query to
cquery, aquery, and invocation evidence as the decision moves from declared
structure to configured structure, planned work, and observed execution.
Check your understanding · 4 questions
1.A first-party query projection places //payments/... and //search/... in separate weakly connected components. Which conclusions are justified?
Select all that apply
2.A library has many direct dependents, but alternate undirected paths still connect the graph when the library node is removed. How should a consultant classify it?
Select one answer
3.bazel query --order_output=deps lists a consumer before its prerequisite. What does that ordering establish?
Select one answer
4.Match each graph concept to the structural question it answers most directly:
Drag each answer onto the matching prompt, or click an answer and then click a prompt
Footnotes
-
The Bazel Query Reference — graph datatype, edge direction, closure operators, graph factoring, and output-order semantics ↩1 ↩2 ↩3 ↩4 ↩5
-
Configurable Query (cquery) — configured-target identity, resolved
select()branches, and universe scope ↩1 ↩2 -
Introduction to the dependency graph — connected components, articulation points, bridges, transitive closure, and topological order ↩1 ↩2
-
Query guide —
somepath,allpaths, reverse dependencies, and dependency-path analysis ↩ -
Extracting build performance metrics — distinct target, configured-target, action, profile, and execution evidence ↩1 ↩2
-
modCommand — the external module-resolution graph and its path-analysis commands ↩