3.1.11 MODULE.bazel Management at Scale
extraBzlmod from 3.1.1 Bzlmod (MODULE.bazel) makes dependency declaration much cleaner than legacy WORKSPACE, but at enterprise scale the hard part shifts from syntax to governance. Because overrides only take effect in the root module, every emergency pin, patch, registry redirect, or local checkout still converges in one dependency-control plane. In the largest repos, the problem is not that MODULE.bazel is hard to read. It is that one file becomes the review and ownership choke point for the whole dependency graph.1,2,3
Unrelated edits collide in review. Language silos also stop mapping cleanly to directory ownership.
Included text behaves as if it were inlined at each include() call. Variable bindings stay file-local. This is not a separate published module and not a substitute for load().
The root module becomes the integration boundary
Root-only overrides are a useful safety property. A dependency cannot quietly smuggle its own override policy into your build, and the integrating repository stays in charge of the final graph.1 The operational consequence is that the integrating repository also owns the pain when dependency policy grows large.
That pain is not hypothetical. A single Bzlmod migration can carry more than 1,000 direct Go dependencies, a MODULE.bazel already around 2,000 lines, and the prospect of 100 to 200 overrides that would be awkward to maintain one by one.3 The organizational shape is the same from another angle: under a single-version policy, the root monorepo governance group or root CODEOWNERS end up responsible for making the shared dependency closure work for everyone.2
Once a repository reaches that point, "add one more bazel_dep" stops being a purely technical edit. It becomes a change to a shared policy surface. That is why teams that used to keep repository rules in team-owned directories often feel a regression when moving to one central MODULE.bazel: the Bazel model got cleaner, but the ownership model got more centralized.3
include() is the scaling valve Bazel gives you
Bazel does not let MODULE.bazel use load(). The supported answer for monorepos with huge root module files is include(), while programmable logic belongs in module extensions instead.4 include() behaves as though another MODULE.bazel-like file were placed at that location, but variable bindings stay local to the file where they were created, rather than leaking across included files.5
Since Bazel 7.2, that gives maintainers a real way to split a large root module into semantic slices instead of treating it as one giant document.6 The common pattern is language- or domain-oriented segmentation such as:
include("//:go.MODULE.bazel")
include("//:js.MODULE.bazel")
That exact style is a better fit for large repos with code-ownership boundaries, and the feature exists to improve structure and reduce merge conflicts in large MODULE.bazel files.7,8
Two details matter in practice:
- Included files must live in the current repository, be referenced with a
//...label, and end with.MODULE.bazel.5 include()is available only to the root module and modules under a non-registry override, so it is mainly a tool for the integrating repository, not a general abstraction published modules can rely on.5,4
That means include() is not a substitute for load(), and it is not a way to build a reusable library of dependency snippets in some other repository. It is a structuring tool for one repo's root dependency policy.5,4
What segmentation fixes, and what it does not
Segmentation fixes the mechanics of editing. A repo can separate Go dependencies from JavaScript dependencies, or keep a dedicated file for security-sensitive overrides, so unrelated changes collide less often in code review and merges.5,7,8 It also makes the file easier to browse: a team can learn "our changes usually live in go.MODULE.bazel" instead of scanning a 2,000-line root file.3,7
What segmentation does not fix is the policy model behind that file.
The remaining gaps are worth naming directly: there is still no grouping construct that applies one override directive to a set of dependencies, the root file still remains the place where organizationally important dependency edits land, and directory-based reviewer ownership no longer maps naturally onto the Bzlmod control plane.3 A common workaround is custom tooling that inspects MODULE.bazel diffs and assigns reviewers dynamically.3
That is the key mental model for this chapter: include() reduces merge-conflict pressure, but it does not decentralize dependency authority. You still have one root module, one effective override policy, and one final place where cross-repo dependency decisions are integrated.1,3,5
Treat scale as an ownership problem, not a syntax problem
The practical playbook is therefore half Bazel, half organization:
- Use
include()to split the root module by semantic area or by the team boundary that most often changes those dependencies.5,7 - Keep the central surface intentionally boring: ordinary
bazel_dep()declarations where possible, overrides only where necessary, and explicit review around files that contain policy exceptions.1,3 - Move ownership and reviewer routing outside the Bazel graph itself. The broader patterns for that live in H.4.2 Stewardship and H.4.4 Review Authority.
- Once dependency churn is continuous, pair that ownership model with automation rather than expecting humans to hand-curate every edit. That is where 6.7.3 Automating Bazel Dependency Updates become the next layer.
include() is structure, not federation. It helps you turn one huge MODULE.bazel into readable semantic segments, and it reduces merge conflicts. But Bzlmod still keeps dependency authority at the root module on purpose. At scale, the winning pattern is a small, segmented control plane plus explicit ownership and automation around it, not a return to scattered, independently managed dependency declarations.
Check your understanding · 3 questions
1.A monorepo's MODULE.bazel has grown to 2,000 lines with 100+ overrides. The team uses include() to split it into go.MODULE.bazel and js.MODULE.bazel. What does this achieve, and what does it NOT achieve?
Select one answer
2.True or false about MODULE.bazel include():
Choose True or False for each sentence
3.Match each scale challenge to the correct response strategy:
Drag each answer onto the matching prompt, or click an answer and then click a prompt
Footnotes
-
Bazel modules — only the root module's overrides take effect, including overrides for transitive dependencies ↩1 ↩2 ↩3 ↩4
-
Multiple external dependency closures in Bazel — single-version policy as a governance model owned by a root monorepo group or
CODEOWNERS↩1 ↩2 -
How Uber Manages Go Dependencies with Bzlmod — 1,000+ direct deps, 2,000-line
MODULE.bazel, 100-200 overrides, ownership mismatch, and dynamic reviewer routing ↩1 ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 -
Frequently asked questions — why
load()is forbidden inMODULE.bazeland why root-moduleinclude()is the supported monorepo-scale alternative ↩1 ↩2 ↩3 -
MODULE.bazel files —
include()semantics, file-local bindings, root/non-registry restriction, and*.MODULE.bazelfile requirements ↩1 ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 -
Migrating to Bazel Modules (a.k.a. Bzlmod) - Module Extensions —
includeavailable as of Bazel 7.2.0 in the WORKSPACE vs MODULE.bazel comparison ↩ -
Bzlmod and Bazel 8 — language-specific
include()pattern (go.MODULE.bazel,js.MODULE.bazel) and code-ownership motivation ↩1 ↩2 ↩3 ↩4 -
State of the Union - Tobias Werth & John Field, Google —
includesupport as a way to improve structure and reduce merge conflicts ↩1 ↩2