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.

From MODULE.bazel to build-visible repositories
Registries expose manifests, MVS fixes versions, then Bazel materializes @repo roots
ROOT MODULE
Declares direct module dependencies only
Written in MODULE.bazel
bazel_dep(name = "protobuf", version = "29.3")
REGISTRY
Serves each module’s MODULE.bazel metadata
BCR and custom registries
Recursive graph discovery
MVS RESOLUTION
Chooses one minimal satisfying version set
Collective, not WORKSPACE-ordered
One resolved version per module
materialize repositories
MODULE REPOSITORY
@rules_java
Mapped from a resolved module
bazel_dep(name = "rules_java", version = "8.6.4")
MODULE REPOSITORY
@protobuf
Mapped from a resolved module
bazel_dep(name = "protobuf", version = "29.3")
EXTENSION REPOSITORY
@maven
Imported with use_repo()
maven_ext = use_extension("@rules_jvm_external//:extensions.bzl", "maven")
use_repo(maven_ext, "maven")
After materialization, BUILD labels use @repo//package:target — module repos and extension repos sit in the same namespace.

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.

key takeaway

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

Answers
MODULE.bazel
Minimal Version Selection
single_version_override / local_path_override
use_repo()

2.True or false about MODULE.bazel and Bzlmod resolution:

Choose True or False for each sentence

MODULE.bazel supports load() statements to pull in helper macros.
Only the root module's overrides take effect in the resolved graph.
MVS may silently upgrade a dependency to the latest registry release even when no one asked for it.

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

0 of 3 answered

Footnotes

  1. External dependencies overview — root module flow, registry lookups, workspace/repository model, and WORKSPACE shortcomings 1 2 3 4 5 6

  2. Bazel modulesMODULE.bazel as module manifest, MVS, overrides, and strict direct-dependency visibility 1 2 3 4 5 6 7 8 9 10 11 12 13

  3. Bazel registries — BCR as default index registry, module metadata, and --registry selection behavior

  4. Dependency Management — WORKSPACE-era transitive dependency pain, one-version rule, and why scalable systems prefer collective resolution 1 2

  5. Minimal Version Selection (Go & Versioning, Part 4) — why MVS picks the minimal satisfying build list instead of arbitrarily newer versions 1 2

  6. MODULE.bazel files — directive surface for module(), bazel_dep(), overrides, use_extension(), and use_repo() 1 2 3 4 5 6

  7. Frequently asked questions — why MODULE.bazel has no load(), why version ranges/latest are unsupported, and why use_repo() is explicit 1 2 3 4

  8. mod Commandgraph, explain, and show_repo for inspecting resolved module state

  9. Bazel LockfileMODULE.bazel.lock, --lockfile_mode, and lockfile collaboration guidance