3.1.2 Using Extensions

Module extensions are how Bzlmod lets third-party ecosystems participate in the same resolved dependency graph as bazel_dep() modules. A ruleset exposes an extension implemented in Starlark. Your MODULE.bazel attaches tags with structured configuration. Bazel gathers those tags from every module that uses the extension, runs the extension implementation once it knows the module graph, and the implementation creates repositories by invoking ordinary repository rules.1 That is the bridge between declarative MODULE.bazel files and the messy reality of Maven coordinates, npm lockfiles, or language-specific resolvers.

The mental model from 3.1.1 Bzlmod (MODULE.bazel) still applies: module resolution happens first, then extension evaluation turns the declared intent into concrete @repo//... labels. What changes is where the programmable work lives — not in MODULE.bazel itself, but in the extension hosted by a dependency module.1,2

The maintainer pattern: use_extension, tags, use_repo

You always start with a bazel_dep on the module that ships the extension, then bind the extension with use_extension, configure it through the tag API the extension defines, and finally import generated repositories with use_repo.1,2

bazel_dep(name = "rules_jvm_external", version = "4.5")
maven = use_extension("@rules_jvm_external//:extensions.bzl", "maven")
maven.install(artifacts = ["org.junit:junit:4.13.2"])
maven.artifact(
    group = "com.google.guava",
    artifact = "guava",
    version = "27.0-jre",
    exclusions = ["com.google.j2objc:j2objc-annotations"],
)
use_repo(maven, "maven")

The extension’s documentation is the source of truth for which tag classes exist (maven.install, maven.artifact, and so on) and which repository names use_repo must import — generated repos are part of the extension’s public API.1 Until a repo is imported with use_repo, labels that point at it are not in your module’s scope even if the extension would create it for another consumer.2

The maintainer-workspace extension wiring is a smaller working copy of the same shape: use_extension on a repo-local .bzl (bound here as policy), one policy.release() tag, and use_repo to import the generated @release_policy repo. The matching policy_extension implementation shows the module_extension + tag_class pair the article describes here.

The production Maven version of that pattern is visible in rules_jvm_external: its Bzlmod guide defines the graph-wide tag and named-repository contract, while the examples/simple/MODULE.bazel file keeps the consumer side small enough to compare directly with the example above.3

How those apparent names relate to canonical repository names is the subject of 3.1.3 Repo Mapping. The repository rules extensions call under the hood are covered in 4.9 Repository Rules, while implementing an extension yourself belongs in 4.10 Authoring Module Extensions.

Tags are aggregated across the module graph

An extension does not see only your module. After resolution, Bazel collects every tag attached to that extension identity from every dependent module, then passes the combined view into the implementation via module_ctx.1 That is what lets two unrelated modules both contribute Maven artifacts or Go modules to one coherent resolver run, instead of duplicating imperative WORKSPACE blocks.

Because the inputs are graph-wide, small tag changes in a dependency can change what your build resolves. When results look wrong, treat the extension like any other external dependency: inspect the resolved module/extension state (bazel mod subcommands) rather than guessing from local tags alone.4

Evaluation is lazy (until you force it)

Module extensions are evaluated lazily: an extension is typically not run unless some module imports one of its repositories with use_repo and that repository is actually needed by the build.1 That keeps unused language ecosystems from downloading the world.

When you are developing or debugging an extension, laziness works against you — nothing runs if nothing references the repos. In that situation bazel mod deps is useful because it evaluates all module extensions unconditionally.1,4

Extension identity is file + name

The same logical extension loaded from two different .bzl files is two different extension identities. If both appear in the transitive graph, Bazel evaluates them separately and each only sees tags wired to that identity.1 Extension authors are expected to expose a single canonical .bzl entry point so consumers do not accidentally fork the tag stream.

Before you chase a “wrong” Maven or npm resolution, confirm you are looking at the same extension identity your tags target, and use bazel mod deps when you need eager evaluation during extension work.1,4 For JVM-heavy brownfield migrations that lean heavily on these patterns, the dedicated migration track is M2 Maven & Gradle → Bazel.

Stop Here or Continue into Authoring

If your job is to configure an existing extension, stop once you can follow its documented tag API, import its public repositories, and inspect evaluation when resolution surprises you. You do not need module_ctx implementation details to be an effective consumer.

Continue to 4.10 Authoring Module Extensions when you must implement or publish an extension, define tag classes, aggregate graph-wide input, choose repository names as public API, or make host and network observations reproducible. That section treats those choices from the ruleset author's side instead of repeating this consumer workflow.

key takeaway

use_extension binds a proxy whose methods emit tags. use_repo imports the repositories the extension promises into your module. Tags are merged across the whole dependency graph, evaluation is lazy by default, and the extension’s .bzl path is part of its identity. Treat these facts as the maintainer checklist whenever you wire a non-Bazel package manager through Bzlmod.

Check your understanding · 3 questions

1.Why is module extension evaluation described as 'lazy by default'?

Select one answer

2.Which of the following are true about how module extensions aggregate tags?

Select all that apply

3.True or false about the use_extension / use_repo pattern:

Choose True or False for each sentence

You can reference repositories generated by an extension without listing them in use_repo().
Referencing the same extension name through two different .bzl paths can create separate extension identities, so multiple tag configurations may be applied.
0 of 3 answered

Footnotes

  1. Module extensions — usage flow, tag aggregation, lazy evaluation, extension identity, and bazel mod deps 1 2 3 4 5 6 7 8 9

  2. MODULE.bazel filesuse_extension, use_repo, and related directives 1 2 3

  3. rules_jvm_external repository map — public extension docs, a minimal Bzlmod consumer, pinned resolution, multi-module contribution diagnostics, and implementation escalation routes

  4. mod Command — inspecting module and extension state from the CLI 1 2 3