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:

  1. Write BUILD files for the service
  2. Declare dependencies explicitly
  3. Fix circular dependencies (Bazel doesn’t allow them)
  4. 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! 🔨