PATTERN Cited by 1 source
Static analysis as cross-repo impact discovery¶
Definition¶
Use static-analysis rules running in every consumer's CI build — not as enforcement gates, but as discovery instruments. The library author writes a rule that detects callsites of their own API surface (deprecated, experimental, internal); the rule's output, aggregated across the fleet, becomes a map of which downstream repos depend on which API surfaces. The library author reads the map to make informed deprecation / removal decisions.
The wiki's first canonical instance is Netflix's Nebula ArchRules post, where the canonical case study is "library authors easily see a report of all downstream consumers using their experimental, deprecated, or non-public APIs."
When to use¶
- You ship a library with downstream consumers in a polyrepo.
- You want to deprecate or remove an API but don't know who depends on it.
- You have a substrate (like patterns/bundled-rules-auto-scoped-to-library-consumers + patterns/centralized-fleet-wide-rule-catalog) that lets you ship rules with the library and aggregate violations centrally.
This pattern inverts the typical static-analysis use case: rules normally enforce; here they observe.
The pattern¶
Library author writes the discovery rule¶
The rule targets the API surface to be discovered. Netflix's canonical example for deprecated-API detection:
ArchRuleDefinition.priority(Priority.MEDIUM)
.noClasses().that(resideOutsideOfPackage(packageName + ".."))
.should()
.dependOnClassesThat(resideInAPackage(packageName + "..").and(are(deprecated())))
.orShould().accessTargetWhere(targetOwner(resideInAPackage(packageName + ".."))
.and(target(is(deprecated())).or(targetOwner(is(deprecated())))))
.allowEmptyShould(true)
.because("Deprecated APIs are subject to removal");
Reading: "No class outside this library's package may depend
on or access a class/member inside this library's package that
is annotated @Deprecated."
Note the Priority.MEDIUM — this rule is not intended
to fail builds. It's intended to report.
Rule travels with the library¶
The rule is bundled with the library it governs (via patterns/bundled-rules-auto-scoped-to-library-consumers). When a consumer pulls the library in as a dependency, the runner discovers the rule via ServiceLoader and runs it.
Aggregation into a dashboard¶
The fleet-wide rule-runner (patterns/centralized-fleet-wide-rule-catalog) streams violations to the Internal Developer Portal:
"Our internal Nebula standard Gradle wrapper and plugin suite automatically enable the ArchRules runner on every project, and provides a custom reporter which sends the report data to our Internal Developer Portal on every main-branch CI build. This way, library authors can easily see a report of all downstream consumers using their experimental, deprecated, or non-public APIs, giving them confidence to make 'breaking' changes, knowing that it will not actually break downstream consumers." — sources/2026-05-08-netflix-scaling-archunit-with-nebula-archrules
Library author reads the dashboard¶
Three actions the dashboard enables:
- Confident removal: zero violations means safe to remove.
- Targeted migration outreach: N violations means a list of specific consumer repos to coordinate with.
- Migration progress tracking: violation count over time shows whether consumers are migrating.
"If their changes are currently blocked by downstream usage, they can easily see exactly which projects are reporting those usages." — sources/2026-05-08-netflix-scaling-archunit-with-nebula-archrules
The inversion¶
Standard library-deprecation flow:
- Mark API
@Deprecated. - Write changelog notice.
- Wait "long enough".
- Remove and hope nothing breaks.
- (Maybe) get paged when a consumer breaks.
This pattern's flow:
- Mark API
@Deprecated. - Ship a bundled rule detecting external callsites.
- Read the dashboard.
- Coordinate with named consumers.
- Remove with confidence (dashboard shows zero).
The structural difference: discovery before action vs action and react to fallout.
Beyond @Deprecated¶
The same shape generalizes to any API-surface attribute:
@Experimental/@Beta— "who's depending on unstable APIs?"- non-
@Public(Netflix's default-internal) — "who's reaching into our library's internals?" - CVE-tagged methods — "who's still calling the vulnerable code path?"
- Performance-sensitive methods — "who's calling this in hot paths?"
- Auth-required methods — "who's bypassing the gateway?"
The technique is machine-checkable downstream-API-usage
discovery; the canonical case is @Deprecated but the
pattern applies broadly.
Distinct from runtime call tracking¶
Some orgs solve API-usage discovery via runtime instrumentation of deprecated methods. Tradeoffs vs static analysis:
| Aspect | Runtime tracking | Static analysis (this pattern) |
|---|---|---|
| Coverage | Only call paths actually executed in production | Every callsite in the codebase |
| Reflection / dynamic dispatch | Caught (the call happens at runtime) | Missed (no static signature) |
| Test-only callers | Caught only if tests run with prod-like config | Always caught |
| Cost | Per-call overhead in production | Build-time only |
| Time to know a consumer migrated | Wait for traffic + window | Next CI build |
| Confidence in safety | High when zero calls in N days | High when zero callsites |
The two are complementary; runtime tracking adds confidence that seemingly-reachable callsites aren't actually exercised.
Adjacent patterns¶
- patterns/api-stability-annotations — the lifecycle-marking discipline that makes deprecated/experimental/internal APIs discoverable.
- patterns/bundled-rules-auto-scoped-to-library-consumers — the substrate that lets the library author ship the rule with the library.
- patterns/centralized-fleet-wide-rule-catalog — the fleet-wide rule-execution + dashboard substrate.
- patterns/build-time-tech-debt-detection — the broader framing this pattern is an instance of.
Hard problems¶
- Reflection / dynamic dispatch / DI / AOP: static analysis misses call paths that aren't visible in bytecode. Library author may underestimate consumer count for libraries consumed via Spring DI, ServiceLoader, or AspectJ.
- Stale dashboards: if a consumer's CI hasn't run recently, their dashboard entry is stale. Library author may underestimate consumer count.
- Coverage of opt-out repos: rules only run in repos that adopt the runner plugin. Repos that opt out (or predate the rollout) are invisible.
- Discovery of indirect dependencies: rules see direct callsites; transitive consumers (e.g. a library that wraps the deprecated API and re-exposes it) are invisible until the wrapper itself is rewritten or rules are written for it.
Seen in¶
- sources/2026-05-08-netflix-scaling-archunit-with-nebula-archrules — first canonical wiki naming. Netflix's case study uses the pattern verbatim: ship rule with library, dashboard aggregates, library author makes confident removal decisions.