2.1.4 Common Dependency Issues
recommendedThe target graph from 2.1.1 Nodes, Edges & Acyclicity is simple, but the failures it produces are often indirect. A build can stay green even while its dependency declarations drift away from the code's real needs, or it can break only after two branches of the graph force incompatible versions of the same external library. The recurring theme is not "Bazel is confused". It is that the graph is telling two different stories: what the code actually uses and what the build metadata or version policy says it uses.1,2,3
1.1.10 Common Error Messages shows how to distinguish Bazel label errors from compiler missing-symbol output and provides a runnable missing-dependency example. Here, the same symptom becomes a graph diagnosis: missing direct edges, accidental re-exports, and version diamonds.
Diamond dependencies become version problems
A diamond dependency is the shape where A depends on B and C, and both of those depend on D. The hard version of the problem is not the shape by itself, but the moment the two branches want incompatible versions of the same external dependency. That is why large Bazel codebases bias toward a one-version policy for third-party code: otherwise some top-level target eventually becomes the place where those requirements collide.2,4
Legacy WORKSPACE-based dependency management made these conflicts painful because transitive external dependencies had to be managed more manually. In Bzlmod, version resolution is collective: Bazel looks across the module graph and uses Minimal Version Selection to choose a reproducible version instead of drifting to whatever a package manager happened to fetch. The deeper mechanics belong to 3.1 Dependency Management, and the brownfield migration story belongs to M1 WORKSPACE → Bzlmod.2,5
Stale dependencies are actual-vs-declared mismatches
The most common day-to-day dependency issue is simpler: the code and the BUILD file disagree about what is direct. Bazel's rule is precise. The graph of actual dependencies must be a subgraph of the graph of declared dependencies, but the declared graph should not grow far beyond what the code really uses.1
If target A starts using symbols from C while its BUILD file still lists only B, the build may keep working for a while because B currently depends on C too. That is a fragile coincidence, not a valid direct-dependency declaration. The failure appears later, when B is refactored and the transitive path disappears. This is exactly why 2.1.3 Dependency Types emphasized "declare direct deps only": missing direct deps make builds brittle, and extra declared deps make the graph noisier than it needs to be.1,2
Re-exports hide the real edge
Re-exports are a specific way to create that fragility. If A gets a symbol from C through B, A is now coupled not just to B, but to part of B's transitive closure. That can increase rebuild surface, and it means A breaks the moment B stops forwarding C, even if A's own source code never changed.3
Sometimes that forwarding is intentional: B is acting as a stable facade. That curated pattern is different, and it shows up later in H.5.1 Shared Components. The problem here is the accidental shortcut, where A conceptually depends on C but relies on B to smuggle it in.3
When one of these issues shows up, start by asking why the path exists:
bazel query 'somepath(//a, //c)'
bazel query 'allpaths(//a, //c)' --output graph
somepath() gives one concrete route through the graph. allpaths() shows every route, which is often the fastest way to spot a diamond or a hidden transitive dependency. If the output is noisy, add --noimplicit_deps to suppress rule-injected and toolchain edges. The beginner tour of these commands is 2.1.5 Inspecting the Graph — Query Preview. The deeper investigation patterns live in 5.2 Query and 5.3 Graph Analysis.6,7,8
Most dependency bugs come from inaccurate or fragile graph declarations. If the edge is real, declare it directly. If two paths want incompatible versions, solve that in dependency management instead of hiding the conflict with ad hoc BUILD-file tricks. If an intermediate target is re-exporting something you use, decide whether it is a real public facade or just hiding the dependency you should name yourself.1,3,4,5
Check your understanding · 3 questions
1.Two parts of a Bazel module graph need different versions of the same external library — a classic diamond conflict. Under Bzlmod, what happens by default?
Select one answer
2.Which of the following are valid ways to investigate a diamond dependency or hidden transitive path in Bazel?
Select all that apply
3.True or false: statements about dependency management and version conflicts.
Choose True or False for each sentence
Footnotes
-
Dependencies — actual vs declared dependencies, overapproximation rule, and the undeclared transitive dependency timeline ↩1 ↩2 ↩3 ↩4
-
Dependency Management — strict direct-dependency hygiene, one-version rule, and the pain of manual transitive external dependency management ↩1 ↩2 ↩3 ↩4
-
Managing dependency graph in a large codebase — diamond dependencies, stale dependency metadata, and re-exports as hidden coupling ↩1 ↩2 ↩3 ↩4
-
Google's One-Version Rule (Third-Party Dependencies) — why conflicting versions eventually collide in a shared dependency graph ↩1 ↩2
-
Bazel modules — diamond dependency resolution with Minimal Version Selection in Bzlmod ↩1 ↩2
-
Query guide —
somepath()andallpaths()for tracing why a target depends on something ↩ -
The Bazel Query Reference —
somepath()returns one arbitrary path,allpaths()returns all paths ↩ -
Query quickstart —
--noimplicit_depsand Graphviz output for cleaner dependency-path visualization ↩