PATTERN Cited by 1 source
Bundled rules auto-scoped to library consumers¶
Definition¶
Co-locate static-analysis rules with the library they govern, in a separate compiled artefact (different jar, classifier, or variant), and have the consumer-side rule runner automatically detect and execute those rules only on source sets that actually consume the library. The rules ride along with the code they govern and need zero per-consumer configuration.
The wiki's first canonical instance is Netflix's Nebula ArchRules Library + Runner plugin pair, deployed across 5,000+ JVM repos.
When to use¶
You're shipping a library in a polyrepo or large-fleet environment where:
- You want to enforce library-specific rules on consumers (e.g. "no class outside this library's package may call deprecated methods inside it").
- You can't expect every consumer to know about and explicitly configure your rules.
- Your rules are tied to library-package-scoped invariants that don't apply to code that doesn't consume the library.
- You have a build tool that supports classifier-variant publication + per-source-set classpath construction (e.g. Gradle with Module Metadata).
Distinguishing characteristic vs standalone rule libraries:
standalone rules apply to all code (e.g. "don't use Guava
Optional"); bundled rules apply only to code that consumes
the rule's parent library.
The pattern¶
Producer side (library author)¶
- Add a separate source set for rules — Netflix's case:
the
archRulessource set added by the Library Plugin. - Implement a rule registration interface —
ArchRulesServicereturningMap<String, ArchRule>. - Build the rules into a separate jar with a distinct
classifier + usage attribute — Netflix's case:
arch-rulesclassifier +usage=arch-rulesattribute via Gradle Module Metadata. - Auto-generate
ServiceLoaderregistration for the rule service (so consumer-side discovery works). - Publish the library with the rules jar as a separate variant alongside the main jar.
Consumer side (library user)¶
- Adopt the runner plugin (in Netflix's case, automatic via the standard Gradle wrapper).
- No per-rule configuration — the runner discovers rules automatically.
Runner-side mechanics (the central novelty)¶
- For each source set in the consuming project (
main,test, custom source sets), the runner constructs a separate rule-classpath configuration combining the source set'sruntimeClasspathwith thearchRulesconfiguration, with thearch-rulesvariant attribute selected. - The runner runs
ServiceLoader<ArchRulesService>against that constructed classpath — pulling in only the rules from libraries that source set actually depends on. - The runner executes those rules in a classpath-isolated Gradle work action scoped to that source set.
- Output: per-rule violations serialized for reporting.
The Netflix MVP¶
"In the following example, we have a Project which uses a test helper library as a testImplementation dependency, and also adds a standalone rules library to the archRules configuration. The test runtime classpath will only contain the implementation jar for the helper library, but the arch rules runtime will contain the archrules jar for the bundled rules and standalone rules. This all happens automatically." — sources/2026-05-08-netflix-scaling-archunit-with-nebula-archrules
The reference implementation is Nebula
Test.
A test-helper library can ship rules like "no test class may
call setUp() directly" — which only matter when the helper
library is consumed.
Why this is the central novelty¶
Without this pattern, an organization wanting fleet-wide rule enforcement has two options:
- Standalone-rules-only model: every consumer must explicitly add every rule library. Doesn't scale (every new rule library requires a coordinated rollout across all consumers).
- Single-blob-of-rules model: one giant rule library that tries to cover everything. Doesn't scope (rules for library A run on consumers that don't use library A; false positives and noise).
The bundled-and-auto-scoped model inverts the configuration direction: the library author owns the rules; the consumer's runner figures out which rules apply based on what the consumer actually uses. Zero configuration on the consumer side.
The library-deprecation use case¶
The canonical Netflix use case (per concepts/breaking-change-detection-via-static-analysis):
- Library author marks API surface with
lifecycle annotations
(
@Deprecated,@Public,@Experimental, default-internal). - Library author bundles a rule that detects calls to deprecated/internal APIs from outside the library's package.
- Rule auto-runs on every consumer's CI.
- Library author reads dashboard, sees who's calling what, decides when to remove the deprecated API.
Without bundled rules, this would require every consumer to opt-in to the library's deprecation rules — defeating the auto-discovery property. Without auto-scoping, the rule would fire on consumers that don't use the library — pure noise.
Adjacent patterns¶
- patterns/api-stability-annotations — the lifecycle-annotation discipline this pattern enables enforcement of.
- patterns/centralized-fleet-wide-rule-catalog — the operational pattern this pattern is a substrate for.
- patterns/static-analysis-as-cross-repo-impact-discovery — the use-case pattern this pattern enables.
Substrate requirements¶
This pattern is not trivially portable to all build tools. Required substrate:
- Variant-based dependency resolution — the consumer's build tool must be able to resolve different jar variants of the same library based on attributes (Gradle Module Metadata supports this; Maven POM does not).
- Per-source-set or per-configuration classpath construction
— the runner needs to walk classpaths separately for
main,test, etc. (Gradle source-set model supports this; older Ant/Maven models don't natively). - Plugin model with ServiceLoader-style discovery — the runner needs a plugin-discovery mechanism that operates on arbitrary classpaths.
In practice, this pins the pattern to Gradle + Module Metadata + ServiceLoader. Maven-only or older-Gradle shops can't adopt without first migrating substrate.
Hard problems¶
- Build-time overhead. Every CI build now runs every bundled rule from every consumed library. At fleet scale this can dominate build time.
- Rule version skew. Different consumers may resolve different versions of the same library, hence different versions of its bundled rules. A rule fix shipped in v1.2 of the library doesn't reach consumers stuck on v1.0.
- High-priority-rule build failures. When a library author ships a new high-priority rule, it can suddenly fail builds across many consumers. Coordination cost falls on whoever ships the new rule.
- Bundled-rules-as-test-pollution. A bundled rule that
fires on
testsource-set classes can produce confusing failures ("why is my test failing because of a rule shipped by a library I depend on?").
Seen in¶
- sources/2026-05-08-netflix-scaling-archunit-with-nebula-archrules — first canonical wiki naming. Netflix's Nebula ArchRules' bundled-rules-auto-scope mechanic is the central design lever; "whenever possible, we recommend writing rules in this bundled way."