3.5.2 Inner-Loop Build Delegation

extra

Bazel is built for correctness across a whole repository, not for the sub-second edit–compile–test cycles that dominate a developer's inner loop. When sub-second iteration matters, some teams treat Bazel as the outer-loop system — authoritative for merges and CI — and delegate the inner loop to the IDE's own incremental compiler instead. This is not a Bazel feature. It is a deliberate decision to step outside Bazel for the minutes of the day when Bazel is not the right tool.1

Why Bazel is a poor fit for the inner loop

Two structural properties of Bazel become a ceiling on interactive speed, especially in a large JVM or polyglot monorepo:1

Per-invocation overhead. Every bazel build pays the cost of the loading and analysis phases from 2.2 Three Phases of a Build before any action runs. On a warm server the numbers are small, but they do not shrink to zero for a trivial one-line edit.

No incrementality within an action. Bazel's caching, covered in 2.4 Caching & Incrementality, operates at action granularity: if any input changes, the whole action reruns. When a source file inside a java_library changes, Bazel re-runs the compile action and javac starts the compile over — even though persistent workers keep the JVM warm, Bazel has no way to resume a compiler mid-flight and reuse its in-memory symbol table at method or symbol granularity. IDE compilers, by contrast, keep that state hot and recompile at method- or symbol-level granularity.

For single-digit-second or sub-second test iteration, these two costs are what "Bazel is too slow" often means in practice.1 The delegation pattern below is a response to that measured latency, not a default replacement for Bazel's local build loop.

The delegation pattern

The pattern has a concrete form in a large (30M-line) JVM monorepo. It resembles the "fast builds" mode of the Bazel IntelliJ plugin (see 3.5.1 IDE Support for baseline plugin setup). However, the actual compilation step is delegated to IntelliJ's incremental compiler. In practice, this requires maintaining a fork of the Bazel plugin so the IDE compiles instead of Bazel.1 The responsibilities are:

  • IDE drives inner-loop compile/run/test. IntelliJ (or any IDE with a real incremental compiler) compiles only what changed, using its symbol graph, and surfaces errors and warnings live.
  • Bazel drives the project model. The plugin tells the IDE which targets exist, which sources they own, and what their classpaths are. New dependencies, generated code, and renamed targets are invisible to the IDE until the project is synced back to Bazel.
  • Bazel remains the source of truth for CI. Every merge still goes through bazel test //... under the CI recipe in 3.6 CI Basics. This is where 2.3 Hermeticity & Sandboxing and sandboxing provide their guarantees, while the remote caching setup in 6.2 Shared Remote Cache provides reusable results.

The reported payoff is large: up to 30× faster incremental builds, with 30-second builds compressing to roughly 1 second.1

An IDE runs its own analyzer rather than invoking Bazel per edit for reasons of latency and feedback quality: asking Bazel to build a single file can invalidate several levels of dependencies and stall for minutes, whereas the IDE resolves direct dependencies at symbol granularity and builds an AST of your code that can also represent errors — so errors, warnings, inspections, and suggested improvements surface live.2 Inner-loop delegation leans on that existing capability instead of fighting it.

Drift is the tax

The cost of this pattern is that two models of the project coexist, and they move apart whenever the build definition changes:1

  • New deps entries in a BUILD file are invisible until the IDE project is re-synced to Bazel.
  • Generated code (protos, GraphQL bindings, stubs) must be produced by Bazel before the IDE can index it.
  • The IDE's compiler flags, annotation-processing setup, and classpath order are not guaranteed to match Bazel's action. A green IDE can hide a red CI.
  • Compute shifts onto developer laptops rather than RBE, so the pattern rewards the fastest available developer hardware.1

Teams that use this pattern reduce the cost of drift by automating BUILD file updates when imports change, providing cross-repo symbol search, and relying on CI to catch whatever the IDE missed.1 Without that investment, time saved by the 30× speedup can be lost to subtle "works on my machine" debugging.

When to reach for this

Inner-loop delegation is a niche pattern, worth knowing exists rather than adopting by default. It pays off most clearly when:

  • The primary language has a mature in-IDE incremental compiler (JVM languages are the canonical case).
  • Local iteration time, not CI time, is the dominant developer complaint.
  • The team already has strong CI gates — otherwise drift leaks into production.

It is a poor fit when the IDE has no real incremental compiler of its own (Bazel C++ setups commonly rely on clangd plus a compile_commands.json, which is a language-server front end rather than a build tool), when generated code dominates the inner loop, or when tests require the hermetic environment to behave the same as in CI.

key takeaway

Bazel's scope is correctness and caching at action granularity — not sub-second iteration. If inner-loop latency is the bottleneck, the realistic option is to delegate fast edit–compile–test cycles to the IDE's own incremental compiler and keep Bazel authoritative for merges and CI. Budget for drift: syncs after new deps and new codegen, plus CI that is trustworthy enough to catch what the IDE missed.1

Check your understanding · 3 questions

1.A JVM team reports that incremental builds with Bazel take 30 seconds while IntelliJ's own compiler takes 1 second for the same change. What is the root cause?

Select one answer

2.True or false about the inner-loop delegation pattern:

Choose True or False for each sentence

In the Airbnb approach, IntelliJ handles the edit-compile-test loop while Bazel remains authoritative for CI and merges.
Inner-loop delegation eliminates the need for CI because the IDE provides equivalent correctness guarantees.
New deps added to BUILD files become visible to the IDE immediately without re-syncing.

3.Inner-loop delegation pays off most clearly in which scenario?

Select one answer

0 of 3 answered

Footnotes

  1. Lessons from a Large JVM Monorepo - Janusz Kudelka, Airbnb — "Minimizing Bazel in the Inner Loop" section: inner-loop vs outer-loop framing, Bazel overhead and no intra-action incrementality, forked Bazel IntelliJ plugin delegating to IntelliJ's incremental compiler, 30s→1s speedup, drift and sync trade-offs, compute shifted to developer laptops. 1 2 3 4 5 6 7 8 9

  2. Bazel plugins in JetBrains IDEs — Justin Kayser on why the IDE runs its own AST and type checker instead of invoking Bazel per edit, to provide live errors, warnings, and inspections.