DEV Community

啸 江
啸 江

Posted on

Why Build Systems Matter: A Deep Dive into Bazel

From "just compile it" to understanding why tools like Bazel exist at scale

The Question Everyone Asks
When developers first encounter build systems like Bazel, the reaction is often the same:
"Why do I need this? I can just compile my code and run it on the server. Isn't this just adding unnecessary complexity?"
It's a fair question. And the honest answer is: you're not wrong — for small projects. But as systems grow, everything changes. Let's walk through why.

You're Already Building, You Just Don't Know It
When you write a Java application and do this:
bashjavac Main.java
jar -cf app.jar *.class
scp app.jar user@server:/app/
java -jar app.jar
Congratulations. You just ran a build pipeline. You compiled, packaged, transferred, and deployed. The difference between this and Bazel is not the concept — it's the scale, reliability, and automation behind it.
The moment your project grows beyond a single developer or a handful of files, doing this manually becomes a nightmare. Here's why.

Real Problems That Build Systems Solve
Problem 1: Dependency Hell
Imagine your project has three modules:
Module A → depends on commons-lang 3.8
Module B → depends on commons-lang 3.12
Module C → depends on both A and B

Which version of commons-lang does Module C use at runtime? What happens when they conflict? Who resolves it?
In a small project, you figure it out manually. In a codebase with thousands of modules and hundreds of third-party libraries, this becomes completely unmanageable by hand.
Bazel solves this by making every dependency explicit and hermetic. Every target declares exactly what it depends on — no implicit assumptions, no "it works on my machine."
python# Bazel BUILD file — crystal clear dependencies
java_library(
name = "module_c",
srcs = glob(["src/*/.java"]),
deps = [
"//module_a",
"//module_b",
"@maven//:commons_lang3", # exact version, pinned
],
)

Problem 2: The "Works on My Machine" Problem
This is the classic developer nightmare:
Developer's laptop: JDK 17, works perfectly
CI server: JDK 11, build fails
Production server: JDK 21, runtime error

Without a build system enforcing consistency, every environment becomes a unique snowflake. Debugging becomes archaeology — digging through layers of environmental differences to find why the same code behaves differently.
Bazel introduces the concept of hermetic builds: given the same source code and the same build rules, Bazel produces bit-for-bit identical output regardless of which machine runs the build.
It does this by:
● Sandboxing each build action from the host environment
● Pinning all toolchains (compilers, linkers, runtimes) explicitly
● Treating the build environment itself as a declared dependency
python# Even the compiler version is declared explicitly
java_toolchain(
name = "jdk17_toolchain",
source_version = "17",
target_version = "17",
java_runtime = "@jdk17//:jre",
)
No more "it works on my machine." Either it works everywhere, or it fails everywhere — and both outcomes are reproducible and debuggable.

Problem 3: Full Rebuilds That Take Hours
Suppose you have 10,000 modules. You change one line in a utility class deep in the dependency graph.
Without a build system:You don't know what's affected. You rebuild everything. At scale, this can take hours.
With Bazel:Bazel maintains a precise dependency graph. It knows exactly which targets are affected by your change and rebuilds only those — nothing more.
You changed: //lib/utils:string_helper

Bazel computes:
→ //service/auth depends on string_helper → rebuild
→ //service/payment depends on string_helper → rebuild
→ //service/logging does NOT depend on it → skip
→ //frontend/dashboard does NOT depend on it → skip

Result: 2 targets rebuilt instead of 10,000

This is called incremental builds, and at Google's scale (where Bazel originated), this capability saves millions of developer-hours per year.
In the CI logs we analyzed, you can see this in action:
INFO: Build completed successfully, 4 total actions

Only 4 actions — not a full rebuild of the entire codebase.

Problem 4: Multi-Language Monorepos
Modern systems rarely live in a single language. A typical large-scale system might look like:
Backend services → Java / Go
Infrastructure → C++
ML pipelines → Python
Frontend → TypeScript
Container images → Dockerfile + shell scripts
Configuration → YAML / Starlark

Without a unified build system, each language has its own toolchain, its own dependency manager, its own CI configuration. Cross-language dependencies become a coordination nightmare.
Bazel handles all of these under one roof:
python# Java service
java_binary(
name = "auth_service",
srcs = glob(["src/*/.java"]),
deps = ["//lib/common:utils"],
)

Go binary

go_binary(
name = "proxy",
srcs = ["main.go"],
deps = ["//lib/config:parser"],
)

Python ML job

py_binary(
name = "training_job",
srcs = ["train.py"],
deps = ["//lib/data:loader"],
)

Docker image that packages the Java service

container_image(
name = "auth_service_image",
base = "@distroless//java",
binary = ":auth_service",
)
One tool. One dependency graph. One source of truth.

Problem 5: Remote Caching and Distributed Builds
Here's a scenario that becomes critical at scale:
Developer A builds module X at 9:00 AM → takes 3 minutes
Developer B tries to build the same module X at 9:05 AM
→ same source code, same inputs
→ why build it again?

Bazel supports remote caching: build outputs are stored in a shared cache (local or cloud). If the inputs haven't changed, the cached result is reused instantly.
Developer B's build:
→ Checks remote cache for module X
→ Cache hit! Downloads result in 2 seconds
→ Skips 3 minutes of compilation entirely

Multiply this across hundreds of developers and thousands of modules, and the time savings are enormous.
Additionally, Bazel supports remote execution: individual build actions can be distributed across a cluster of machines, running in parallel. A build that would take 30 minutes on a single machine might finish in under 3 minutes when distributed across 50 workers.

Problem 6: Dependency Tree Analysis at Scale
This is something you can observe directly in modern CI systems. Before deploying a change, teams need to answer:
"If I update library X from version 1.0 to version 2.0, which services are affected?"
Without a build system, this question requires manually tracing through documentation, grep-ing through codebases, and hoping nothing was missed.
With Bazel, the dependency graph is a first-class artifact:
bash# Show everything that depends on //lib/auth:token_validator
bazel query "rdeps(//..., //lib/auth:token_validator)"

Output:

//service/api:server
//service/admin:dashboard
//service/mobile:gateway
//integration_tests:auth_suite
Instantly. Accurately. At any scale.
This is exactly what the dep_tree tooling in the CI logs we examined does — generating a complete, machine-readable dependency graph for every component, enabling impact analysis before any deployment.

Problem 7: Enforcing Architecture Boundaries
In large organizations, you often want to enforce rules like:
"The payment service must not directly depend on the logging internals.""Frontend packages cannot import backend business logic."
With Bazel, these rules can be encoded directly into the build:
python# This target is only visible to specific packages
java_library(
name = "internal_crypto",
srcs = glob(["src/*/.java"]),
visibility = [
"//service/payment:pkg",
"//service/auth:pkg",
# Nobody else can depend on this
],
)
If any unauthorized module tries to depend on internal_crypto, the build fails immediately — before any code reaches production. Architecture governance becomes automated and enforced, not just documented and hoped for.

The Restaurant Analogy
Think of it this way.
Cooking dinner for yourself: you open the fridge, grab what's there, cook it. Simple. No process needed.
Now you're running a restaurant with 100 chefs:
● Who sources the ingredients, and from where?
● How do you guarantee every dish tastes the same regardless of which chef makes it?
● If one ingredient runs out, which dishes are affected?
● How do you train a new chef to produce consistent results?
You need standardized processes. Not because processes are fun, but because without them, the complexity becomes unmanageable.
Bazel is that standardization layer for software at scale.

Summary
The concern
The reality
"I'm already building fine without it"
You are building — just manually, which doesn't scale
"It adds unnecessary complexity"
It replaces invisible, unmanaged complexity with explicit, controlled complexity
"It increases maintenance overhead"
At scale, it dramatically reduces maintenance overhead
"I can just compile and deploy"
You can — until you can't, and then the pain is severe

The One-Line Answer
Build systems don't make simple things complicated. They make things that are already extremely complicated — manageable.
Once a project crosses a certain threshold of size, team count, or language diversity, the question is no longer whether you need a build system. It's which one, and how well you use it.
Bazel is one answer to that question — designed for the scale where the problems described above aren't theoretical. They're daily reality.

Written based on hands-on experience analyzing large-scale CI systems powered by Bazel.

Top comments (0)