Adopting Bazel's Bzlmod for Polyglot Dependency Management
Learn how Bzlmod replaces WORKSPACE with declarative, version‑locked dependencies and see a concrete polyglot example that shows setup, verification, and the trade‑offs involved.
27 Dec 2025, 17:57 UTC

Problem: WORKSPACE sprawl in a polyglot repo
When a repository contains Java, Go, and C++ code, the traditional WORKSPACE file often becomes a long list of git_repository, http_archive, and custom macro calls. Each language team may pin different versions, and transitive dependencies can introduce duplicate copies or conflicting versions. The result is a build that is hard to reproduce across machines and CI agents, and updating a dependency requires manual edits in multiple places.
Thesis: Bzlmod gives deterministic, version‑locked dependencies
Bazel’s Bzlmod system, enabled by default starting with Bazel 7.0, replaces the imperative WORKSPACE with a declarative MODULE.bazel file. Dependencies are declared with semantic versioning, and the Minimal Version Selection (MVS) algorithm resolves them to a single compatible version per module. The resolved graph is recorded in a lockfile (MODULE.bazel.lock) that, when committed, guarantees the same external repositories are fetched on every machine, improving hermeticity and reproducibility.
How Bzlmod works
Each module appears as a entry in the Bazel Central Registry or a custom index. In MODULE.bazel you write:
bazel_dep(name = "rules_go", version = "0.40.0")
bazel_dep(name = "rules_jvm_external", version = "5.0")
bazel_dep(name = "rules_cc", version = "0.1.1")
Running bazel mod tidy fetches the selected versions, evaluates any module extensions (e.g., maven_install or go_deps), and writes MODULE.bazel.lock with the exact commit hashes or version numbers that were chosen. The lockfile is the single source of truth for the external repository cache.
Worked example: Java, Go, and C++ dependencies
- Declare dependencies in
MODULE.bazel:bazel_dep(name = "rules_jvm_external", version = "5.0") bazel_dep(name = "rules_go", version = "0.40.0") bazel_dep(name = "rules_cc", version = "0.1.1") # Module extensions that generate language‑specific repositories use_extension(@rules_jvm_external//:extensions.bzl, "maven") maven_install( name = "maven", artifacts = ["org.apache.commons:commons-lang3:3.14.0"], repositories = ["https://repo1.maven.org/maven2"] ) use_extension(@rules_go//:extensions.bzl, "go") go_register_toolchains(version = "1.22.0") go_deps(name = "go_deps") # Local C++ toolchain (could be from rules_cc) use_repo_rules(@rules_cc//:toolchain.bzl, "cc_toolchain") cc_toolchain(name = "my_toolchain") - Resolve and lock by running (requires read access to the network and write access to the Bazel cache):
bazel mod tidyThis command creates or updates
MODULE.bazel.lock. You should inspect the file to confirm that the versions you expect are present and that no unexpected downgrades occurred. - Commit the lockfile to version control. Treat it as immutable; any change must go through
bazel mod tidyagain. - Cache the external repository in CI. The default location is
~/.cache/bazel/_bazel_$USER. Restoring this directory between jobs avoids re‑fetching archives and speeds up incremental builds. - Build the whole repository:
bazel build //...Because the lockfile fixes the exact external repositories, the build graph is deterministic across developers and CI agents.
Trade‑off and limitation
Adopting Bzlmod introduces a learning curve for teams accustomed to the macro‑heavy WORKSPACE style. Custom repository rules must be rewritten as module extensions, which can be non‑trivial if they rely on complex logic. Moreover, not all rule sets have published Bzlmod‑compatible extensions yet; some still require a WORKSPACE.bzlmod shim or direct WORKSPACE usage. Teams should consult each rule set’s release notes or the Bazel Central Registry to verify support before depending on it.
Actionable closing
Start with a small, self‑contained subsystem (for example, a Java library that only depends on rules_jvm_external). Add its dependencies to a new MODULE.bazel, run bazel mod tidy, and commit the lockfile. Verify the graph with bazel mod graph and confirm that bazel build --subcommands //:target produces the same action digests on a clean machine as on your developer workstation. Once the subsystem builds consistently, expand the migration module by module, keeping the legacy WORKSPACE.bzlmod file alongside MODULE.bazel during the transition. When you are confident that the lockfile reproduces the build, you can remove the shim and rely entirely on Bzlmod.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.