Why We Chose Bazel
Our build times were out of control. A full rebuild of our monorepo took over an hour. Incremental builds weren’t much better because our dependency graph was poorly understood.
We needed a build system that:
- Scaled to millions of lines of code
- Provided hermetic, reproducible builds
- Offered aggressive caching
- Supported multiple languages (Go, Python, TypeScript, Rust)
- Enabled distributed builds
Bazel checked all these boxes.
The Bazel Promise
Fast: Only rebuild what changed Correct: Hermetic builds guarantee reproducibility Multi-language: One build system for everything Scalable: Google uses it for their billions of lines of code
Our Journey
Phase 1: Proof of Concept
We started with a single Go microservice:
# BUILD.bazel
load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library", "go_test")
go_library(
name = "auth",
srcs = ["auth.go"],
deps = [
"//pkg/database:db",
"@com_github_golang_jwt_jwt_v4//:jwt",
],
)
go_binary(
name = "auth-service",
embed = [":auth"],
)
go_test(
name = "auth_test",
srcs = ["auth_test.go"],
embed = [":auth"],
)
The explicit dependency declaration was verbose but powerful.
Phase 2: Expanding Coverage
We incrementally added more services to Bazel. The pattern emerged:
- Write BUILD files for the service
- Declare dependencies explicitly
- Fix circular dependencies (Bazel doesn’t allow them)
- Run
bazel build //...to verify
Phase 3: Remote Caching
This is where Bazel really shines. We set up a remote cache:
# .bazelrc
build --remote_cache=grpc://cache.company.com:9092
build --remote_timeout=60s
Now builds across the entire team share cached artifacts. If anyone built a target, everyone benefits.
Phase 4: Remote Execution
Taking it further, we set up remote execution:
build --remote_executor=grpc://executor.company.com:9092
Builds now run on powerful remote machines instead of developer laptops.
What We Learned
The Good
Incremental builds are magic: Change one file, rebuild in seconds instead of minutes.
Reproducibility is real: “Works on my machine” disappeared. If it builds in CI, it builds everywhere.
Dependency clarity: Explicit dependencies forced us to understand our architecture better.
Language-agnostic: Unified build system across Go, Python, and TypeScript.
The Challenging
Learning curve: Bazel’s model is different. It takes time to think in “hermetic builds.”
Migration effort: Converting existing projects requires significant upfront investment.
Tooling gaps: IDE integration isn’t as smooth as language-native tools.
Debugging: When builds fail, error messages can be cryptic.
Key Patterns We Developed
1. Macro Libraries
We created macros for common patterns:
# //build/defs.bzl
def microservice(name, srcs, deps = []):
go_library(
name = name + "_lib",
srcs = srcs,
deps = deps,
)
go_binary(
name = name,
embed = [":" + name + "_lib"],
)
go_test(
name = name + "_test",
srcs = [name + "_test.go"],
embed = [":" + name + "_lib"],
)
2. Gazelle for Go
Gazelle auto-generates BUILD files for Go code:
bazel run //:gazelle
This reduced the manual effort significantly.
3. Remote Cache Strategy
We configured different cache backends:
- Local disk cache for developers
- Shared network cache for CI
- Read-only cache for external contributors
Build Time Improvements
Before Bazel:
- Clean build: 65 minutes
- Incremental build: 8-15 minutes
- Cache hit rate: 0% (no caching)
After Bazel:
- Clean build (cold cache): 45 minutes
- Clean build (warm cache): 3 minutes
- Incremental build: 30 seconds - 2 minutes
- Cache hit rate: 85-95%
Advice for Teams Considering Bazel
Do Use Bazel If:
- You have a large monorepo (>100k LOC)
- Build times are a pain point
- You need reproducible builds
- You work across multiple languages
- You have the resources to invest in the migration
Don’t Use Bazel If:
- You have a small project (<10k LOC)
- Language-native tools work fine
- Your team isn’t ready for the learning curve
- You need quick wins (Bazel is a long-term investment)
The Bottom Line
Bazel isn’t a silver bullet. It trades simplicity for power. You need to invest significant effort upfront, but the payoff is substantial for large-scale projects.
For us, Bazel transformed our development workflow. Build times dropped dramatically, and reproducibility issues vanished.
But we’re a large engineering organization with the resources to support it. Smaller teams might find the complexity outweighs the benefits.
Choose your build system based on your actual needs, not hype. And if you choose Bazel, commit to doing it right - half-measures won’t give you the benefits.
Resources
Happy building! 🔨