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
Returns MyLibraryInfo
Returns MyLibraryInfo
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.
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
CcInfo tells C++ rules that they may treat the target as C++-compatible.CcInfo directly whenever it carries C++ data internally.CcInfo in a narrower provider can prevent accidental direct C++ consumption.Footnotes
-
Rules — dependency attributes specify required providers. Targets expose providers returned by implementation functions. ↩
-
Writing Bazel rules: library rule, depsets, providers —
go_libraryaccepts any dependency returningGoLibraryInfo, then readsdep[GoLibraryInfo]. ↩ -
attr —
providerslist-of-lists semantics and deprecation ofallow_rulesin favor of providers. ↩ -
Provider — provider symbols act as constructors and as keys for
target[Provider]access. ↩ -
.bzl style guide — rule design guidance for documented provider interfaces and extensibility. ↩
-
Integrating with C++ Rules — custom Starlark rules can provide
CcInfoso C++ rules can depend on them. ↩ -
CcInfo —
CcInfois a C++ compilation/linking provider and marking provider. ↩ -
Integrating with C++ Rules — wrapping C++ data in
JavaCcInfowhen direct C++ consumption is not intended. ↩ -
rules_spring repository map — the public dependency-filter guide and focused provider regression tests bound this production example. ↩