4.11.1 Ruleset Layout & Public Entry Points

A reusable ruleset has two audiences: Bazel, which loads the .bzl files, and humans, who decide what is safe to copy. The repository layout is part of that API. It tells users which files are public entry points, which setup snippets are supported, and which helpers are internal implementation detail.1

Ruleset layout separates supported API from internals
Public files teach users what to load; private files keep implementation changeable.
rules_mylang/

Repository layout communicates the support contract.

Supported public entry points

Documented and versioned. Only usage snippets are copied.

Module metadata + usage
MODULE.bazel — ruleset metadata
README.md — copyable setup
Public package
mylang/
defs.bzl
extensions.bzl
repositories.bzl — legacy WORKSPACE
Public-path proof
examples/
BUILD.bazel
tests/
integration/
Generated docs
docs/
defs.md
extensions.md
generated from public .bzl
Private internals

Undocumented, movable, refactorable

Implementation helpers
mylang/private/
compile_impl.bzl
toolchain_impl.bzl
providers.bzl
internal/
download.bzl
versions.bzl
Change boundary
Users do not load these files
Maintainers can rename helpers
Tests guard the public paths
Copy README setup snippets and load documented defs.bzl / extensions.bzl; treat private / internal files as implementation detail.

The Repository Teaches The Contract

The official guidance starts with a naming convention: publish reusable rules in their own repository, commonly named $ORGANIZATION/rules_$NAME, with a clear repository description and top-level README.2 That is not just cosmetics. A ruleset is usually found through a registry, GitHub, or a copied bazel_dep() snippet. The name and README are the first compatibility signal users see.

At the top level, MODULE.bazel defines the module name users depend on. The deploying guide's example keeps that name stable and shows the release announcement snippet users should paste into their own MODULE.bazel:

bazel_dep(name = "rules_mylang", version = "1.2.3")

Changing the module name after users adopt it is therefore a migration event, not a refactor. 4.11.3 Module Metadata & Version Semantics explains the detailed version and compatibility semantics, but the layout should already make the dependency entry point obvious.

Public .bzl Files Are Doorways

A ruleset may contain many .bzl files, but users should not have to guess which ones are safe to load. The deploying guide recommends a language or domain package with a defs.bzl entry point that exports all public rules.3 In practice, a mature ruleset often has several public doorways:

The bazel-contrib scaffold demonstrates that boundary in one production-shaped tree: its mylang/ package keeps defs.bzl, extension, repository, and toolchain entry points above a clearly separate private/ implementation directory.4 Treat its sample rule and smoke target as placeholders. The repository is evidence for maintainable layout and release plumbing, not a tutorial for implementing a language rule.

mylang/
  defs.bzl          # rules, macros, public providers
  extensions.bzl    # Bzlmod module extensions
  repositories.bzl  # legacy WORKSPACE setup during migration

The exact filenames are illustrative. What matters is that the supported path is documented, stable, and easy to find. If users need mylang/private/toolchain_impl.bzl to make the first example work, the ruleset has leaked an implementation file into its public API.

A minimal version of this pattern keeps implementation symbols under internal/, while def.bzl loads and re-exports the public go_binary symbol so downstream users do not need to update their load() statements when internals move.5 The same idea extends to providers: GoLibraryInfo and other provider symbols are public API when compatible downstream rules need to consume or implement them.6

This continues the rule-level API design from 4.4.1 Rule Public API Design. There, attrs, providers, outputs, and docs become stable faces of one rule. Here, the public .bzl entry points become the stable face of the whole ruleset: they decide which rule symbols, provider symbols, module extensions, and setup helpers downstream code may rely on.

Internals Need A Place To Move

Private implementation files are not bad. They are how a ruleset stays maintainable. Put action-building helpers, low-level repository rules, generated-version tables, toolchain implementation details, and provider plumbing behind private paths or clear naming conventions. Users can still read the source, but the layout should make unsupported loads feel obviously unsupported.

That separation gives maintainers room to reorganize internals without breaking every downstream load() statement. Public entry points can re-export stable symbols while private files split, merge, or move. The compatibility policy in 4.11.6 Rule Compatibility & Bazel Version Policy should define how public attrs, providers, generated repo names, and setup helpers evolve. Internal helper names should not need the same promise.

Setup Is Part Of The Layout

Rulesets do not only publish rule symbols. They also publish setup paths: bazel_dep(), module-extension tags, use_repo() names, toolchain registration, and sometimes legacy WORKSPACE macros while users migrate. Keep those setup files near the public API and document the intended order.

Toolchains make this especially visible. The deploying guide notes that registered toolchain() targets are always analyzed during toolchain resolution, while the implementation targets behind their toolchain attribute are only needed when that toolchain is actually selected. It recommends splitting the repository that contains toolchain declarations from the repository that contains heavier implementation targets when registration would otherwise force unnecessary fetching.7 That design becomes the toolchainization pattern in 4.11.2 Toolchainization.

Release artifacts add one more boundary: the source repository layout and the distributed archive do not have to be identical. A common release technique uses git archive plus .gitattributes to stamp version files, omit bulky folders such as examples from the shipped archive, and keep release creation tied to a tag.8 4.11.4 Publishing Rulesets develops the release mechanics. The layout decision here is which directories are source-only support material and which are part of the published module.

Examples And Docs Prove The Public Path

Examples and tests should exercise the public entry points, not private shortcuts. The official layout includes top-level tests/ and optional examples/ directories, with examples showing basic ways users can apply the rules.9 The executable-documentation side was covered in 4.5.3 Example Workspaces as Contract Tests: an example workspace should look like a downstream consumer, load public .bzl files, and run the commands the README teaches.

Generated API documentation closes the loop. Public .bzl files and public functions should carry docstrings, while rules, aspects, attributes, providers, and provider fields should use doc metadata so documentation tools have structured API text to extract.10 The newer BCR documentation flow lets rulesets publish generated Starlark docs with the module via docs_url metadata.11 Keep the docs close to the entry points they describe, and make CI check that the published docs and examples still match the public surface. 4.5.4 Stardoc — API Documentation explains generation, and 4.11.5 Ruleset CI/CD Patterns turns it into a release gate.

key takeaway

A ruleset layout should answer three questions without a maintainer in the room: what should users load, how should they enable the ruleset, and which files are implementation detail?

Put public symbols behind documented entry points such as defs.bzl and extension setup files. Keep private helpers out of the supported load path. Then use examples, tests, and generated docs to prove that the documented public path works.

Check your understanding · 3 questions

1.Match each ruleset path to the role it should usually play.

Drag each answer onto the matching prompt, or click an answer and then click a prompt

Answers
MODULE.bazel
mylang/defs.bzl
mylang/private/compile_impl.bzl
examples/BUILD.bazel

2.Which choices help keep a reusable ruleset's public API stable?

Select all that apply

3.True or false: public and private boundaries in ruleset layout.

Choose True or False for each sentence

A source repository may contain support directories that are not shipped in the released module archive.
If a helper is under internal/ or private/, users can still safely depend on its load path because Bazel can read it.
Generated docs are most useful when they describe the public .bzl entry points that users are meant to load.
Provider symbols never need to be part of a public entry point because providers are only used inside one rule implementation.
0 of 3 answered

Footnotes

  1. Deploying Rules — repository layout, MODULE.bazel, README, defs.bzl, tests, examples, and docs as the reusable ruleset surface.

  2. Deploying Rules — hosting and naming guidance for $ORGANIZATION/rules_$NAME, repository descriptions, tags, and README titles.

  3. Deploying Rules — language/domain package containing BUILD and public defs.bzl entry point.

  4. rules-template repository map — public entry points, private helpers, consumer-shaped smoke tests, toolchain repositories, and release automation in the maintained scaffold.

  5. Writing Bazel rules: simple binary rule — public def.bzl re-exports go_binary from internal/rules.bzl to keep user load() statements stable.

  6. Writing Bazel rules: library rule, depsets, providers — public def.bzl re-exports both rules and GoLibraryInfo for compatible downstream rules.

  7. Deploying Rules — toolchain registration can force analysis/fetching of toolchain targets. Split declaration repos from heavy implementation repos when needed.

  8. Publishing Bazel rules that depend on tools: take 2git archive, .gitattributes, version stamping, excluding examples, and inserting release integrity hashes. Releasing Bazel rulesets that publish tools — tag-triggered release automation and source-vs-release archive behavior.

  9. Deploying Rules — top-level tests/ and optional examples/ directories for verifying and demonstrating reusable rules.

  10. .bzl style guide — docstrings for public files/functions and doc metadata for rules, aspects, attrs, providers, and provider fields.

  11. Bazel Starlark Docs on the Registrystarlark_doc_extract, bzl_library, docs_url, and BCR rendering for published Starlark API docs.