Skip to content

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

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:

  1. Confident removal: zero violations means safe to remove.
  2. Targeted migration outreach: N violations means a list of specific consumer repos to coordinate with.
  3. 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:

  1. Mark API @Deprecated.
  2. Write changelog notice.
  3. Wait "long enough".
  4. Remove and hope nothing breaks.
  5. (Maybe) get paged when a consumer breaks.

This pattern's flow:

  1. Mark API @Deprecated.
  2. Ship a bundled rule detecting external callsites.
  3. Read the dashboard.
  4. Coordinate with named consumers.
  5. 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

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

Last updated · 542 distilled / 1,571 read