3.1.4 Legacy WORKSPACE Model

recommended

Legacy WORKSPACE is the dependency model behind most pre-Bzlmod Bazel guides and many brownfield repositories. Instead of declaring a module graph and resolving it collectively, the main repo bootstraps external repositories by executing repo rules in WORKSPACE, often followed by load()ed setup macros.1,2 You still need that mental model when reading older docs, understanding why a repo behaves differently from 3.1.1 Bzlmod (MODULE.bazel), or deciding what still has to move into M1 WORKSPACE → Bzlmod.

What to Recognize in Old Docs

A typical legacy setup introduced each external repository directly in WORKSPACE:1

Legacy
load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")

http_archive(
    name = "foo",
    urls = ["https://example.com/foo.zip"],
    sha256 = "c9526390a7cd420fdcec2988b4f3626fe9c5b51e2959f685e8f4d170d1a9bd96",
)

In real repositories that stanza was often followed by more load() statements from the downloaded repo and helper macros like *_dependencies() or *_toolchains().1,2 That is the big difference from 3.1.1 Bzlmod (MODULE.bazel): WORKSPACE behaved more like an imperative bootstrapping script than a declarative dependency manifest.

That model scaled badly. Bazel did not evaluate the WORKSPACE files of your dependencies, so transitive repos had to be recreated in the main repo or hidden behind layered deps.bzl macros.1 Because the file is evaluated sequentially, macro order could also affect which transitive dependency version actually got used.1,2 The same load()-driven shape is why brownfield repos still run into the fetch behavior discussed in 3.1.5 Eager Fetch Anti-pattern.2

Why It Still Shows Up

Legacy WORKSPACE is no longer the direction of travel, but it is not gone yet. Bazel 8 turned it off by default, and Bazel 9 removes it, yet older tutorials, partially migrated rulesets, and long-lived internal repos still depend on it.2,3 Some projects also keep an empty or comment-only WORKSPACE file around because older tools still look for it as a repository boundary marker even after real dependency logic moved elsewhere.1,4 If that is the only role the file still plays, the root-marker part of the story was introduced earlier in 0.1.2 Repository Root.

Coexistence Is the Real Brownfield Problem

When both systems are enabled, the practical question is not "which file runs first?" but "what does @name resolve to from this repository?" In the main repo, names introduced by the root MODULE.bazel win first, and Bazel falls back to WORKSPACE repos only when no Bzlmod mapping claims that name.5 But repositories in the Bzlmod world do not see WORKSPACE-defined repos at all.2,5

That asymmetry is where mixed-mode bugs hide. A label may still work from the main repo while failing from a module dependency or extension-generated repo. If WORKSPACE.bzlmod exists, Bazel uses it instead of WORKSPACE when Bzlmod is enabled, which makes it a useful scratchpad for the legacy repos you have not migrated yet.2,6

Brownfield Hygiene

Treat legacy WORKSPACE as inventory, not as the place where new dependency logic should keep growing. Keep a short record of which targets or rulesets still rely on WORKSPACE-only repos, migrate direct dependencies one by one, and remove duplicate declarations once a dependency is stable in MODULE.bazel.2,4 Duplicating the same dependency path in both worlds is how "polluted workspace" failures happen: an older repo definition from WORKSPACE can unexpectedly survive long enough to shadow or conflict with the migrated setup.4,6

If you actively need to support both dependency modes for consumers, the detailed tactics live in M1.3 Dual Compatibility (WORKSPACE + Bzlmod). This article's job is smaller: recognize the old pattern, understand the visibility trap, and keep the remaining WORKSPACE surface area contained. The full reference and migration playbook is M1 WORKSPACE → Bzlmod.

key takeaway

Legacy WORKSPACE is still operationally relevant, but it is no longer the long-term source of truth. Recognize the repo-rule plus load() pattern, assume mixed-mode visibility is subtle, and treat every remaining WORKSPACE stanza as migration debt to isolate or remove.

Check your understanding · 3 questions

1.In a repo with both Bzlmod and legacy WORKSPACE enabled, a label @foo//... is used from a module dependency. What visibility rule applies?

Select one answer

2.True or false about legacy WORKSPACE:

Choose True or False for each sentence

WORKSPACE is evaluated lazily: only the stanzas needed by a given build are executed.
WORKSPACE.bzlmod can serve as a scratchpad for legacy repos not yet migrated under Bzlmod.
Bazel 8 removed WORKSPACE support entirely.

3.What is the biggest architectural difference between WORKSPACE and Bzlmod dependency resolution?

Select one answer

0 of 3 answered

Footnotes

  1. External dependencies overview — legacy WORKSPACE syntax, http_archive() example, boundary markers, and shortcomings around transitive deps and sequential evaluation 1 2 3 4 5 6

  2. Bzlmod Migration Guide — WORKSPACE vs MODULE.bazel, hybrid migration, WORKSPACE.bzlmod, and repository visibility rules 1 2 3 4 5 6 7 8

  3. Bazel 8.0 LTS Release Blog PostWORKSPACE off by default in Bazel 8 and planned removal in Bazel 9

  4. Migrating to Bazel Modules (a.k.a. Bzlmod) - The Easy Parts — gradual migration, duplicate dependency information trade-offs, and keeping a minimal WORKSPACE for older tools 1 2 3

  5. Frequently asked questions — how @name resolves when both --enable_bzlmod and --enable_workspace are set 1 2

  6. Moving to Bzlmod — polluted workspace behavior and WORKSPACE.bzlmod as a cleaner dual-mode boundary 1 2