5.3.3 Target Boundary Design
Target boundary design is the choice of which build concepts receive stable, named interfaces in the declared graph. A useful boundary separates dependency surfaces, consumer groups, ownership, or change policy. Its value begins with architecture: a target split can clarify those relationships even when it does not make a build faster.
search
shared
Linux config
total
each
Do consumers use pieces separately?
Does batching or the input set shrink?
Do direct deps or consumers narrow?
Do added units repay their cost?
Do Not Equate Directories, Files, and Graph Nodes
A Bazel package is a directory containing a BUILD or BUILD.bazel file. It
can declare one rule target, many rule targets, or no rule that directly
corresponds to the directory as a whole. Bazel does not impose a
one-target-per-directory convention. The official query tutorial, for example,
puts four independent ingredient libraries in one package.1
Files can also appear as targets in the unconfigured query graph. A source file
named in srcs is a source-file target, and an output declared by a rule is a
generated-file target. That does not make every filesystem file an
automatically analyzed build node, nor does it make every source file an
independent compilation or cache unit. The query graph contains rule and file
targets connected by rule declarations. The rule implementation later decides
which actions consume those files.2
This yields three related but different views:
| Layer | How the chosen target boundary appears | Possible resulting effect |
|---|---|---|
| Target graph | rule targets and their declared dependency edges | more labels, APIs, visibility decisions, and deps edges |
| Configured graph | label-plus-configuration instances | configuration-specific dependency surfaces |
| Action graph | operations with declared inputs and outputs | execution boundaries, if the rules actually register different actions |
One label can have multiple configured instances, and configured targets expose
the actions produced by analysis. query, cquery, and aquery therefore
answer different granularity questions.3 5.3.1 Graph Theory Foundations
establishes those graph layers. A split in the target graph does not guarantee a
corresponding split in the action graph: a rule may batch many sources into one
action, register several actions, or register none. After selecting a boundary
that makes structural sense, 5.4.3 Measuring Granularity Trade-offs shows how to test
its performance consequences rather than predicting them from label count.
Design Boundaries Around Dependencies and Interfaces
The most useful split usually separates code that has different dependencies,
different consumers, or a meaningful API boundary. Start with a bounded graph
observation: which consumers reach the candidate target, which direct edges
cross the proposed boundary, and which dependency is present only for one part?
Suppose //catalog:catalog
contains pricing and search code. If only pricing uses a large tax library, a
split into //catalog:pricing and //catalog:search lets search consumers avoid
that dependency. The improvement is not that the new labels are smaller. It is
that the dependency edge and propagated provider surface can now be narrower
and independently governed.
Direct dependency hygiene makes the boundary enforceable. Each target should name the libraries its own sources use instead of inheriting an accidental transitive path through a neighbor. Google's BUILD-file automation experience notes that known direct Java dependencies let the compiler receive the required jars instead of an unnecessarily broad transitive closure. Unused dependencies otherwise add work and coupling.4 The same source also shows the migration cost: splitting a large rule is only the first step, because downstream consumers must be moved to the smallest appropriate new interface.4
Consumer sets provide a practical test. If nearly every consumer still needs
both pieces, the split may not express a real public interface. If distinct
consumer groups need distinct pieces, the new targets make that separation
visible in deps, rdeps, visibility, and ownership policy. Keep compatibility
aliases or aggregate targets only where they serve a deliberate migration or
public entry point; otherwise they can preserve the broad dependency surface
the split was meant to remove.
An ABI boundary can narrow invalidation even when target boundaries remain
unchanged. Java rules can propagate interface jars for compilation instead of
full implementation jars. JavaInfo.compile_jars may contain those interface
jars, and the API recommends them for Java-only consumers.5 If an
implementation-only edit leaves the interface artifact unchanged, downstream
compile actions can retain an unchanged input. That is a different mechanism
from creating more targets: target granularity controls declared modularity,
while ABI artifacts control which producer changes become visible to consumer
actions. L1.10 ABI Jars & Transitive Dep Pruning treats the JVM-specific mechanism in depth.
This distinction prevents a common overcorrection. Per-file targets are not the only route to precise incrementality. A stable interface artifact, pruned direct dependencies, or actions with genuinely smaller declared input sets can matter more than the number of source files named by a rule.
Make the Interface and Its Stewardship Explicit
Use a split when it expresses a stable distinction that consumers can use:
- the pieces have different direct dependencies.
- consumers commonly need one piece without the other.
- the split creates a real API, visibility, ownership, or release boundary.
- different teams need an explicit ownership or change-review boundary.
Keep or restore a coarser boundary when consumers nearly always need every piece or the split mostly creates BUILD metadata, forwarding targets, and ambiguous ownership. File-level dependency inference can offer high precision without asking humans to maintain every per-file edge, but it requires language-aware tooling and a policy for generated edges.6
Verify the structural result before discussing speed: the intended consumers
should depend on the intended public target, unnecessary direct edges should be
gone, visibility should express the supported access pattern, and ownership
should say who may change the interface. A raw query deps(...) node count is
not this verification; inspect bounded paths and consumer sets, then explain the
specific edges that changed.2 If the proposal also claims faster clean or
incremental builds, test that separate claim with the controlled workflow in
5.4.3 Measuring Granularity Trade-offs.
Bazel does not require one target per directory, and filesystem files do not automatically become independent build units. Design target boundaries around real dependency surfaces, distinct consumer sets, stable interfaces, visibility, and maintainable ownership. Keep ABI-based shielding distinct from target count: it can protect consumers from implementation changes without creating another public target. Verify a split by inspecting its direct edges, paths, consumers, access policy, and migration result. Any performance benefit is a separate hypothesis that requires a controlled measurement.
Check your understanding · 4 questions
1.A ten-source library is split into ten rule targets, but aquery still shows one compile action consuming all ten sources. What is the best assessment?
Select one answer
2.//catalog:catalog contains pricing and search code. Only pricing uses a tax library, and many consumers need search alone. Which facts support splitting the target?
Select all that apply
3.Evaluate these claims about where graph granularity appears:
Choose True or False for each sentence
4.A Java library keeps the same public interface jar after an implementation-only edit, so downstream compile actions retain the same input. What does this demonstrate?
Select one answer
Footnotes
-
Query quickstart — multiple
java_libraryrule targets declared in one ingredients package ↩ -
The Bazel Query Reference — rule, source-file, and generated-file targets in the unconfigured dependency graph ↩1 ↩2
-
Configurable Query (cquery) — label-plus-configuration identity, analysis cost, and access to configured-target actions ↩
-
Automating Build Files — direct-dependency pruning, rule sharding, and downstream migration costs ↩1 ↩2
-
JavaInfo — interface compile jars versus full compile jars for downstream Java compilation ↩
-
The anatomy of a dependency graph — file-versus-package detail, dependency inference, and rebuild-versus-cache-lookup trade-offs ↩