Skip to content

CONCEPT Cited by 1 source

Breaking-change detection via static analysis

Definition

A use-case framing for fitness-function-style static analysis: instead of "deprecate-and-pray" — mark an API @Deprecated, hope nobody depends on it, remove it after some unspecified grace period — ship a static-analysis rule that detects callsites of the deprecated API across all consumers, and read the dashboard to know exactly when it's safe to remove.

The wiki's first canonical instance is Netflix's ArchRules post:

"After a Netflix incident relating to a library releasing a backwards-incompatible change, our team was asked to provide some tooling and practices to improve the Java library lifecycle management. This was not a simple case of a library making a reckless breaking change. The code removed had been deprecated for years."

The structural problem

In a polyrepo, the library author sees:

  • The library's own code.
  • Maybe a few flagship consumers they happen to know about.

The library author does not see:

  • Most actual consumers.
  • Which consumers call which deprecated methods.
  • Whether consumers have migrated, are blocked, or never started.

Standard practice is to mark APIs @Deprecated, write a deprecation notice in the changelog, wait "long enough", and remove. The problem: "long enough" is unknowable without consumer-discovery tooling.

The static-analysis solution

The library author writes a fitness function (in Netflix's case, an ArchUnit rule):

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."

When this rule ships bundled with the library (patterns/bundled-rules-auto-scoped-to-library-consumers), it auto-runs on every consumer's CI build. Failures aggregate to the dashboard. The library author reads the dashboard and gets a precise list of which consumer repos still depend on which deprecated API surfaces.

The user-facing payoff

"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. 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

Three new capabilities:

  1. Confident removal — when the dashboard shows zero consumers, the deprecated API can be removed.
  2. Targeted migration outreach — when the dashboard shows N consumers, the library author has a concrete list of teams to coordinate migration with.
  3. Migration progress tracking — over time, the dashboard should trend down as consumers migrate.

Generalization beyond @Deprecated

The same shape works for any API surface attribute:

  • @Experimental"who depends on unstable APIs?"
  • non-@Public (i.e. internal-by-default in Netflix's taxonomy) — "who depends on internal APIs they shouldn't be using?"
  • @RequiresAuth(...)"who calls auth-sensitive methods without going through the gateway?"
  • CVE-vulnerable methods"who's still calling Runtime.exec(String)?"
  • Performance-sensitive methods"who calls Stream.collect(toList()) in hot paths?"

The concept is machine-checkable downstream-API-usage discovery, with @Deprecated as the canonical case.

Distinct from binary-compatibility validation

Aspect Binary-compat validator (e.g. systems/kotlin-binary-compatibility-validator) Breaking-change detection via static analysis
Where it runs The library's own CI Each consumer's CI
What it tells you This change would break ABI This change will break specific consumers
Discovery surface Library-side ABI signature dump Fleet-side dashboard of actual usage
Granularity Single library's surface Cross-fleet consumer count
Safety net Pre-release Post-deprecation, pre-removal

Binary-compat validators stop you from accidentally breaking ABI. Breaking-change detection tells you whether it's safe to intentionally break ABI by removing the deprecated API.

Distinct from runtime usage tracking

Some orgs solve this with runtime tracking: instrument the deprecated API, log every call, aggregate. Tradeoffs:

Aspect Runtime tracking Static analysis
Coverage Only call paths actually exercised in production Every callsite in the codebase
Cost Runtime overhead per call Build-time only
Latency Wait for the call to actually happen Immediate (next CI build)
Confidence in safety High (zero calls in N days = probably safe) High (zero callsites = definitely safe to compile)
Branches not covered by tests Will miss them Catches them

Static analysis is preventive (catches all callsites including ones not exercised); runtime tracking is observed (catches what's actually called). They're complementary; runtime tracking adds confidence that even reachable-but-unexercised callsites aren't actually used in production.

Limitations

  • Reflection / dynamic dispatch — calls via Class.forName
  • Method.invoke aren't visible to bytecode analysis. The rule misses them.
  • Service loader / DI / AOP — calls discovered at runtime via ServiceLoader, Spring DI, or AspectJ aren't visible statically.
  • Code outside the rule's scope — third-party JAR consumers, scripts, build-tool plugins don't run the rule in their CI. The rule only sees consumers that have adopted the ArchRules runner plugin.
  • Stale dashboard — if a consumer's CI hasn't run recently, their dashboard entry is stale; library author may underestimate consumer count.

These limitations don't invalidate the concept but bound its applicability.

Seen in

Last updated · 542 distilled / 1,571 read