4.2.8 Provider-as-Interface Pattern

Provider-as-interface is the rule-authoring habit of treating a provider as the compatibility boundary between targets. A consuming rule should usually ask "does this dependency expose the information I need?" rather than "was this dependency created by the rule kind I expected?" That is why deps attributes are usually constrained with provider requirements, and why provider fields become part of a ruleset's public API.1

Provider contracts are the compatibility boundary
Different rule kinds interoperate through one documented MyLibraryInfo field contract.
Producer rule kinds
Implementation can vary
Returns provider instances
my_library rule

Returns MyLibraryInfo

generated_library rule

Returns MyLibraryInfo

Provider contracts
Interface stays stable
Documents fields and meaning
MyLibraryInfo
headers objects
Consumer rule
Dependency check is the contract
Accepts compatible targets
deps attr
deps = attr.label_list(
providers = [MyLibraryInfo],
)
implementation
dep[MyLibraryInfo].headers
The consumer names providers = [MyLibraryInfo], so any target that returns MyLibraryInfo is compatible, regardless of its rule kind.

4.2.7 Custom Provider Declaration introduced how to declare a provider. The design consequence is that once another rule reads dep[MyInfo], MyInfo is no longer just an implementation detail. It is the interface other rule authors can implement, consume, document, and test.

Rule Kind Is Not The Interface

A rule kind describes how a target is analyzed. A provider describes what the analyzed target offers to consumers. Those are related, but they are not the same boundary.

go_library = rule(
    implementation = _go_library_impl,
    attrs = {
        "deps": attr.label_list(
            providers = [GoLibraryInfo],
        ),
    },
)

4.2.7 Custom Provider Declaration explains how to declare the provider itself. What matters here is the boundary expressed by the attribute: go_library.deps does not require dependencies to be created specifically by go_library. It accepts any target that returns GoLibraryInfo.2 A second rule can therefore produce generated Go code and participate in the same dependency graph without pretending to be go_library.

The providers parameter on attr.label() and attr.label_list() is the analysis-time gate for this contract. A dependency must return all providers from at least one accepted provider set. The older allow_rules rule-kind filter is deprecated in favor of provider requirements.3 Use provider requirements when compatibility depends on available data, not on the producer's implementation name.

Providers Are Public API

Provider fields need the same care as rule attributes. The producer promises field names, value shapes, and semantics. The consumer builds actions from those values. Bazel's Provider object is both the constructor for provider instances and the key used to access them on a Target, so the symbol itself is the capability that connects producers and consumers.4

Export the provider symbol from the public .bzl entry point if other rulesets should implement compatible producers. Keep helper providers private if they only coordinate internal targets. That visibility choice is the compatibility policy: a public symbol invites independent producers and consumers. A private symbol reserves the protocol for implementation details. The official Starlark style guide is explicit here: pass information between rules through a well-defined provider interface, declare and document provider fields, and design rules so other rules can interact with them.5

This also changes how you think about evolution. Renaming a provider field, changing a depset to a list, or changing the meaning of "headers" can break consumers even if every BUILD file still parses. If you need richer data, prefer adding a new field or a new provider over silently changing the meaning of an existing field.

Built-In Providers Follow The Same Pattern

CcInfo is the clearest built-in example. C++ rules expose CcInfo for compilation and linking data, and custom Starlark rules can return CcInfo so C++ rules can depend on them.6 The consumer does not need to know whether the target came from native cc_library, a custom archive rule, or another ruleset. It needs the C++ provider contract.

That power cuts both ways. The CcInfo API documentation calls it a marking provider: returning it tells C++ rules they may depend on your target. If that is not the intended semantic relationship, wrap the CcInfo inside a narrower provider instead of returning it directly.7

JavaCcInfo = provider(
    doc = "Native C++ data carried through Java targets.",
    fields = {
        "cc_info": "CcInfo needed by a later Java-aware consumer.",
    },
)

The C++ integration guide uses this exact shape: if a Java rule only wants to propagate native dependencies up to a Java binary, it should wrap the C++ data in something like JavaCcInfo, because cc_binary depending directly on java_library does not make sense.8 The public provider you return answers "who is allowed to treat this target as compatible?"

Design Consumers Around Contracts

When writing a consumer rule, put the contract in the attribute schema and then write the implementation against the promised provider data:

def _my_binary_impl(ctx):
    transitive_headers = [
        dep[MyLibraryInfo].headers
        for dep in ctx.attr.deps
    ]
    # Declare actions from provider data here.

Together, the attribute schema and implementation make the interface visible: the consumer constrains dependencies by provider, then uses only the data promised by that provider. Provider declaration and access mechanics are covered in 4.2.7 Custom Provider Declaration. Here they establish substitutability between producer rule kinds.

Avoid custom rule-kind checks in the implementation. They are fragile, they block compatible third-party producers, and they duplicate what the attribute schema can express. If there are multiple acceptable contracts, use the provider list-of-lists form deliberately, then branch on provider presence only where the rule truly supports multiple interfaces.

Provider-as-interface is also why DefaultInfo is not enough for language-level composition. DefaultInfo.files tells Bazel which files are default outputs, and runfiles tell executable targets what they need at runtime. A compiler, linker, packager, or code generator usually needs semantic data: import paths, headers, libraries, source maps, generated descriptors, or tool-specific metadata. That semantic data belongs in a domain provider.

rules_spring demonstrates the same contract with a built-in ecosystem provider. Its deps_filter.bzl accepts targets that provide JavaInfo, filters their compile and runtime jars, and returns a reconstructed JavaInfo for downstream Java consumers.9 The rule does not require one producer rule kind. Compatibility is expressed by the provider it consumes and preserves.

In the mini-ruleset, GlyphInfo is advertised by glyph_library, required by its deps and exports, and read through those provider-bearing edges in rules.bzl.

key takeaway

Depend on providers, not rule kinds. A provider is the interface: producers return it, consumers require it, and both sides agree on documented fields and semantics.

Return broad ecosystem providers such as CcInfo only when you want broad ecosystem compatibility. If you only need to carry that data through an intermediate target, wrap it in a narrower provider so the target does not accidentally advertise the wrong interface.

Check your understanding · 3 questions

1.What does the provider-as-interface pattern ask a consuming rule to depend on?

Select one answer

2.Which design choices follow from treating providers as public interfaces?

Select all that apply

3.True or false: CcInfo as an interface boundary.

Choose True or False for each sentence

Returning CcInfo tells C++ rules that they may treat the target as C++-compatible.
A rule should return CcInfo directly whenever it carries C++ data internally.
Wrapping CcInfo in a narrower provider can prevent accidental direct C++ consumption.
A provider requirement can allow different producer rule kinds to satisfy the same consumer contract.
0 of 3 answered

Footnotes

  1. Rules — dependency attributes specify required providers. Targets expose providers returned by implementation functions.

  2. Writing Bazel rules: library rule, depsets, providersgo_library accepts any dependency returning GoLibraryInfo, then reads dep[GoLibraryInfo].

  3. attrproviders list-of-lists semantics and deprecation of allow_rules in favor of providers.

  4. Provider — provider symbols act as constructors and as keys for target[Provider] access.

  5. .bzl style guide — rule design guidance for documented provider interfaces and extensibility.

  6. Integrating with C++ Rules — custom Starlark rules can provide CcInfo so C++ rules can depend on them.

  7. CcInfoCcInfo is a C++ compilation/linking provider and marking provider.

  8. Integrating with C++ Rules — wrapping C++ data in JavaCcInfo when direct C++ consumption is not intended.

  9. rules_spring repository map — the public dependency-filter guide and focused provider regression tests bound this production example.