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.

A target split changes structure — not outcomes by itself
Declared targets multiply. The requested closure determines configured targets, while rule behavior determines actions.
ONE COARSE TARGET
Sources share one declared boundary
One configured target is reached in this example
Sources
price
search
shared
Target
:catalog
Configured
:catalog
Linux config
Actions
0…n
total
FOCUSED TARGETS
Sources receive separate declared boundaries
Three configured targets are reached in the same example configuration
Sources
price
search
shared
Targets
:pricing
:search
:shared
Configured
:pricing
:search
:shared
Actions
0…n
each
A useful boundary must change what matters
API boundary
Do consumers use pieces separately?
Action boundaries + inputs
Does batching or the input set shrink?
Fan edges
Do direct deps or consumers narrow?
Analysis + action overhead
Do added units repay their cost?
Do not infer fewer actions, more cache hits, or smaller rebuilds from source grouping alone.

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:

LayerHow the chosen target boundary appearsPossible resulting effect
Target graphrule targets and their declared dependency edgesmore labels, APIs, visibility decisions, and deps edges
Configured graphlabel-plus-configuration instancesconfiguration-specific dependency surfaces
Action graphoperations with declared inputs and outputsexecution 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.

key takeaway

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

A target split adds labels and declared dependency boundaries at the target layer.
One label always produces exactly one configured-target instance.
A configured rule target may register zero, one, or several actions.
A source-file target is automatically an independent compilation and cache unit.

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

0 of 4 answered

Footnotes

  1. Query quickstart — multiple java_library rule targets declared in one ingredients package

  2. The Bazel Query Reference — rule, source-file, and generated-file targets in the unconfigured dependency graph 1 2

  3. Configurable Query (cquery) — label-plus-configuration identity, analysis cost, and access to configured-target actions

  4. Automating Build Files — direct-dependency pruning, rule sharding, and downstream migration costs 1 2

  5. JavaInfo — interface compile jars versus full compile jars for downstream Java compilation

  6. The anatomy of a dependency graph — file-versus-package detail, dependency inference, and rebuild-versus-cache-lookup trade-offs