3.1.1 Bzlmod (MODULE.bazel)
Bzlmod is Bazel's module-based dependency system. Instead of building external dependency state by imperatively running repository rules in WORKSPACE, the root of the repo declares direct dependencies in MODULE.bazel, Bazel asks registries for the referenced modules' own MODULE.bazel files, resolves the whole graph once, and then turns the result into external repositories that BUILD files can use.1,2 That separation is why modern Bazel dependency management feels more like a package manager and less like a bootstrapping script.
MODULE.bazel declares intent, not the whole closure
A typical root module names the project and lists only the modules it directly depends on:
module(name = "acme", version = "1.0")
bazel_dep(name = "rules_java", version = "8.6.4")
bazel_dep(name = "protobuf", version = "29.3")
That is intentionally shorter than the old WORKSPACE pattern. You do not spell out the full transitive closure yourself. Bazel starts from the root MODULE.bazel, recursively reads dependency modules from registries, and only then decides which versions to use.1,2 By default those modules come from the Bazel Central Registry. Details about mirrors and downloader policy come later in 3.1.6 BCR Infrastructure. Custom registry hosting and registry layout live in 3.1.8 Registry Structure & Custom Registries.3 The maintainer-workspace MODULE.bazel is the same shape in a runnable repo: a single module() declaration, three bazel_dep() lines, and a local extension wired in with use_extension / use_repo.
Resolution is collective, not imperative
This is the biggest mental shift from 3.1.4 Legacy WORKSPACE Model. In the WORKSPACE world, transitive external dependencies were often reproduced manually or hidden inside helper macros, and the effective result could depend on ordering and convention.1,4 In Bzlmod, Bazel first discovers the full module graph and then resolves versions across that graph as one problem.1,2
The resolution algorithm is Minimal Version Selection (MVS). "Minimal" does not mean "pick the oldest thing in the registry." It means "pick the earliest version that satisfies all declared minimum requirements," which in practice is the highest version requested anywhere in the dependency graph, but not some newer release that nobody asked for.2,5 If one branch needs D 1.0 and another needs D 1.1, Bazel resolves to D 1.1. It does not silently jump to D 1.2 just because it exists.2,5
This default fits Bazel's broader one-version bias for third-party code. The normal outcome is one resolved version per module, which keeps the graph reproducible and avoids the "which copy did I really link against?" mess that grows around unmanaged diamonds.2,4 When you truly need an exception, you reach for an override instead of hoping transitive resolution will sort itself out.
After resolution, modules become repositories
Bzlmod still ends up with repositories, because BUILD files and labels need repositories to point at. The difference is that repository definitions are produced after module resolution, not handwritten up front.1,2 That is why the same dependency system can give you a clean bazel_dep() interface while still materializing @rules_java//... or @protobuf//... labels for the build graph.1 Those labels ultimately attach to targets and edges in the build graph model from 2.1 Directed Acyclic Graph (DAG).
This also explains strict visibility. A module can directly see the repositories provided by its own bazel_dep() declarations, any extension-generated repositories it explicitly imports with use_repo(), and any repositories it declares locally with use_repo_rule().2,6 Transitive dependencies may exist in the resolved graph, but they are not meant to be casually referenced by name from arbitrary modules. That keeps external dependency usage closer to "declare what you use" instead of relying on whatever some intermediate dependency happened to pull in. The naming and mapping details show up later in 3.1.3 Repo Mapping.2
When a BUILD file reaches for a repository the root module never declared, the failure looks like an ordinary label error but the fix belongs in MODULE.bazel. The no-such-package-error snippet keeps that external-repo case separate from a missing local package so the two diagnostics do not blur together.
Overrides are the escape hatch
Most dependencies should be boring bazel_dep() declarations. Overrides exist for the cases that are not boring: pinning a version, forcing a different registry, applying patches, allowing multiple versions to coexist, or replacing a registry-backed module with an archive, Git repo, or local checkout.2,6
bazel_dep(name = "rules_python", version = "1.6.3")
local_path_override(module_name = "rules_python", path = "../rules_python")
The important constraint is that only the root module's overrides take effect.2,6 That makes overrides a deliberate maintainer tool, not something every transitive dependency can smuggle into your build. Use them here for local development, compatibility fixes, and brownfield migration. M1 WORKSPACE → Bzlmod develops the migration playbook, and 4.9 Repository Rules explains the repository-rule mechanics underneath these escape hatches.2,6
Why MODULE.bazel stays declarative
MODULE.bazel is intentionally less programmable than WORKSPACE. It does not support load(), version ranges, or "latest" dependency declarations.7 The reason is architectural: Bazel starts from the local root MODULE.bazel, then resolves dependency modules by reading their MODULE.bazel files from registries before those source archives are fetched, so dependency declaration has to stay simple, inspectable, and reproducible.2,7
When you need programmable behavior, Bzlmod does not give up and fall back to ad hoc scripting. It moves that logic into module extensions. The common maintainer pattern is: declare a ruleset as a module, configure its extension with tags, then import the repositories that extension generates with use_repo().6,7
maven = use_extension("@rules_jvm_external//:extensions.bzl", "maven")
maven.install(artifacts = ["com.google.guava:guava:33.2.1-jre"])
use_repo(maven, "maven")
3.1.2 Using Extensions covers day-to-day use. Continue to 4.10 Authoring Module Extensions to author extensions, understand their evaluation model, and debug their implementation.6,7
Treat MODULE.bazel as the input, not the full truth. bazel mod graph shows the resolved module graph, bazel mod explain <module> tells you why a module or version is present, and bazel mod show_repo <repo> shows the repository definition Bazel synthesized from that resolution.8 Keep MODULE.bazel.lock in version control as well: it records the registry inputs and extension outputs Bazel used, speeds up future resolution, and helps keep dependency state reproducible across the team.9 The maintainer-workspace MODULE.bazel.lock shows the on-disk shape — lockFileVersion, then registryFileHashes Bazel pinned during resolution. Pinning that resolved slice also reduces avoidable churn in the wider incremental graph described in 2.4 Caching & Incrementality.
Bzlmod separates declaration from resolution. MODULE.bazel says what your repo directly needs. Registries and MVS turn that into one resolved dependency graph. Bazel then materializes the repositories and extension-generated repos the build can actually reference. Once that model clicks, the rest of dependency management is mostly special cases: overrides when you need an escape hatch, extensions when a non-Bazel ecosystem must participate, and migration work when an old WORKSPACE world still has to coexist.
Check your understanding · 3 questions
1.Match each Bzlmod concept to its description:
Drag each answer onto the matching prompt, or click an answer and then click a prompt
2.True or false about MODULE.bazel and Bzlmod resolution:
Choose True or False for each sentence
3.Your root module declares bazel_dep(name = "rules_jvm_external", ...), then configures its maven extension with a list of artifacts. To reference @maven//:guava from a BUILD file, what additional MODULE.bazel directive is required?
Select one answer
Footnotes
-
External dependencies overview — root module flow, registry lookups, workspace/repository model, and WORKSPACE shortcomings ↩1 ↩2 ↩3 ↩4 ↩5 ↩6
-
Bazel modules —
MODULE.bazelas module manifest, MVS, overrides, and strict direct-dependency visibility ↩1 ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9 ↩10 ↩11 ↩12 ↩13 -
Bazel registries — BCR as default index registry, module metadata, and
--registryselection behavior ↩ -
Dependency Management — WORKSPACE-era transitive dependency pain, one-version rule, and why scalable systems prefer collective resolution ↩1 ↩2
-
Minimal Version Selection (Go & Versioning, Part 4) — why MVS picks the minimal satisfying build list instead of arbitrarily newer versions ↩1 ↩2
-
MODULE.bazel files — directive surface for
module(),bazel_dep(), overrides,use_extension(), anduse_repo()↩1 ↩2 ↩3 ↩4 ↩5 ↩6 -
Frequently asked questions — why
MODULE.bazelhas noload(), why version ranges/latest are unsupported, and whyuse_repo()is explicit ↩1 ↩2 ↩3 ↩4 -
modCommand —graph,explain, andshow_repofor inspecting resolved module state ↩ -
Bazel Lockfile —
MODULE.bazel.lock,--lockfile_mode, and lockfile collaboration guidance ↩