Most audits read code; this one reads documentation first and then traces each
promise to every implementation path that should honor it. The bugs caught here
are not concurrency or arithmetic defects — they are quiet contradictions
between what the javadoc promises and what the code does.
Build a list of behavioral promises from public API docs, then trace each one
to every code path that should honor it.
Sources of contracts to enumerate:
Caffeine.java — every <b>Note:</b> and <b>Warning:</b> block in a
builder method's javadoc. Pay particular attention to:
weakKeys, weakValues, softValues — equality semantics flip to
identity (==) for the configured strength
expireAfter*, refreshAfterWrite — duration constraints, what
"expired" means at boundaries
maximumSize, maximumWeight — eviction triggering and amortization
removalListener, evictionListener — when and how notifications fire
Cache.java, LoadingCache.java, AsyncCache.java, AsyncLoadingCache.java
— javadoc of every method, especially "must", "will", "guarantees",
"for any reason" language.
Policy.java — every method's documented behavior.
Weigher.java, Expiry.java, RemovalListener.java, RemovalCause.java
— user-facing contracts referenced by the cache.
- Internal obligations — "callers must" contracts stated in
.claude/rules/*.md and in internal-class javadoc across all modules (e.g.
jcache's EventDispatcher requires every publishing thread to drain via
awaitSynchronous/ignoreSynchronous; async operations must register with the
in-flight set). Enumerate each obligation, then verify every call site honors
it — including executor-thread and refresh paths that don't flow through the
obvious entry points.
For each contract, record the exact wording, the configurations that activate
it, and the set of operations affected.
Then trace each promise through every code path that should honor it:
- Direct API calls on
Cache / LoadingCache / AsyncCache
asMap() view methods (size, isEmpty, containsKey, containsValue,
get, put, remove(k), remove(k,v), replace, compute*, merge,
equals, hashCode)
asMap().keySet() / values() / entrySet() — contains, remove,
removeAll, retainAll, removeIf, iterator, spliterator
- Bulk operations (
putAll, getAll, getAllPresent, invalidateAll)
AsyncCache.synchronous() round-trip — does the sync view honor the same
contract as the async cache?
- Serialization round-trip — does the deserialized cache honor the same
promises about expiration durations, loader presence, etc.?
A path that uses a different equivalence, ordering, or visibility from what
the contract promises is a contract drift finding. So is a path that
silently downgrades a configuration on round-trip.
Common drift patterns to check explicitly:
- Identity vs. equals on weak/soft caches:
weakKeys/weakValues/
softValues promise identity, but view collections that use
o.equals(value) or Collection.contains(value) may apply the user's
equality semantics instead.
- Cardinality vs. presence asymmetry: methods documented as treating
in-flight async values as absent must align with
size, isEmpty,
equals, hashCode, and iterators on the same view.
- Notification "for any reason":
removalListener documented to fire
for any cause, but async paths may drop notifications for null or
exceptional completions.
- Cross-version serialization: serialized fields renamed or defaulted
differently across versions can silently lose configuration or throw
on round-trip.
- Same-instance return semantics: e.g.,
compute(k, (k,v) -> v) is
documented one way but updates timestamps/weight regardless.
size() documented as estimate: this is an explicit out — verify it
is actually documented as an estimate everywhere it diverges from logical
presence.
- Synchronous vs. asynchronous view divergence: the
synchronous()
view's asMap() may treat in-flight entries inconsistently across query,
mutation, and cardinality methods.
Each finding should anchor both sides of the drift — the source of the
contract (file + javadoc snippet) and the divergent implementation (file +
method) — and include a minimal user-observable scenario where the docs and
the code disagree.
1---2name: audit-contract-drift3description: Find places where documented API contracts and the implementation diverge4---56Most audits read code; this one reads documentation first and then traces each7promise to every implementation path that should honor it. The bugs caught here8are not concurrency or arithmetic defects — they are quiet contradictions9between what the javadoc promises and what the code does.1011Build a list of behavioral promises from public API docs, then trace each one12to every code path that should honor it.1314Sources of contracts to enumerate:1516- `Caffeine.java` — every `<b>Note:</b>` and `<b>Warning:</b>` block in a17 builder method's javadoc. Pay particular attention to:18 - `weakKeys`, `weakValues`, `softValues` — equality semantics flip to19 identity (`==`) for the configured strength20 - `expireAfter*`, `refreshAfterWrite` — duration constraints, what21 "expired" means at boundaries22 - `maximumSize`, `maximumWeight` — eviction triggering and amortization23 - `removalListener`, `evictionListener` — when and how notifications fire24- `Cache.java`, `LoadingCache.java`, `AsyncCache.java`, `AsyncLoadingCache.java`25 — javadoc of every method, especially "must", "will", "guarantees",26 "for any reason" language.27- `Policy.java` — every method's documented behavior.28- `Weigher.java`, `Expiry.java`, `RemovalListener.java`, `RemovalCause.java`29 — user-facing contracts referenced by the cache.30- **Internal obligations** — "callers must" contracts stated in31 `.claude/rules/*.md` and in internal-class javadoc across all modules (e.g.32 jcache's EventDispatcher requires every publishing thread to drain via33 awaitSynchronous/ignoreSynchronous; async operations must register with the34 in-flight set). Enumerate each obligation, then verify every call site honors35 it — including executor-thread and refresh paths that don't flow through the36 obvious entry points.3738For each contract, record the exact wording, the configurations that activate39it, and the set of operations affected.4041Then trace each promise through every code path that should honor it:4243- Direct API calls on `Cache` / `LoadingCache` / `AsyncCache`44- `asMap()` view methods (`size`, `isEmpty`, `containsKey`, `containsValue`,45 `get`, `put`, `remove(k)`, `remove(k,v)`, `replace`, `compute*`, `merge`,46 `equals`, `hashCode`)47- `asMap().keySet()` / `values()` / `entrySet()` — `contains`, `remove`,48 `removeAll`, `retainAll`, `removeIf`, `iterator`, `spliterator`49- Bulk operations (`putAll`, `getAll`, `getAllPresent`, `invalidateAll`)50- `AsyncCache.synchronous()` round-trip — does the sync view honor the same51 contract as the async cache?52- Serialization round-trip — does the deserialized cache honor the same53 promises about expiration durations, loader presence, etc.?5455A path that uses a different equivalence, ordering, or visibility from what56the contract promises is a **contract drift** finding. So is a path that57silently downgrades a configuration on round-trip.5859Common drift patterns to check explicitly:6061- **Identity vs. equals on weak/soft caches**: `weakKeys`/`weakValues`/62 `softValues` promise identity, but view collections that use63 `o.equals(value)` or `Collection.contains(value)` may apply the user's64 equality semantics instead.65- **Cardinality vs. presence asymmetry**: methods documented as treating66 in-flight async values as absent must align with `size`, `isEmpty`,67 `equals`, `hashCode`, and iterators on the same view.68- **Notification "for any reason"**: `removalListener` documented to fire69 for any cause, but async paths may drop notifications for null or70 exceptional completions.71- **Cross-version serialization**: serialized fields renamed or defaulted72 differently across versions can silently lose configuration or throw73 on round-trip.74- **Same-instance return semantics**: e.g., `compute(k, (k,v) -> v)` is75 documented one way but updates timestamps/weight regardless.76- **`size()` documented as estimate**: this is an explicit out — verify it77 is actually documented as an estimate everywhere it diverges from logical78 presence.79- **Synchronous vs. asynchronous view divergence**: the `synchronous()`80 view's `asMap()` may treat in-flight entries inconsistently across query,81 mutation, and cardinality methods.8283Each finding should anchor both sides of the drift — the source of the84contract (file + javadoc snippet) and the divergent implementation (file +85method) — and include a minimal user-observable scenario where the docs and86the code disagree.