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:
- Confident removal — when the dashboard shows zero consumers, the deprecated API can be removed.
- Targeted migration outreach — when the dashboard shows N consumers, the library author has a concrete list of teams to coordinate migration with.
- 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.invokearen'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¶
- sources/2026-05-08-netflix-scaling-archunit-with-nebula-archrules — Netflix's canonical use case, with the deprecated-API-detection rule as the worked example.