The {@code get(key, mappingFunction)} signature matches Caffeine's manual-cache
+ * contract: callers supply a key-aware {@code Function} and failures are exposed as
+ * unchecked exceptions.
+ */
+@ProviderType
+public interface Cache {
+
+ /** Return the cached value, or {@code null} if absent. */
+ @Nullable
+ V getIfPresent(@NotNull K key);
+
+ /**
+ * Return the cached value. If absent, invoke {@code mappingFunction}, cache the
+ * result, and return it.
+ *
+ * Matches Caffeine's manual-cache contract: the mapping function receives
+ * the cache key and failures are exposed as unchecked exceptions.
+ *
+ * @param key the key whose associated value is to be returned
+ * @param mappingFunction the function used to compute a value if absent
+ * @return the current (existing or computed) value, or {@code null} if the
+ * mapping function returns {@code null}
+ */
+ @Nullable
+ V get(@NotNull K key, @NotNull Function super K, ? extends V> mappingFunction);
+
+ /** Unconditionally put a value into the cache. */
+ void put(@NotNull K key, @NotNull V value);
+
+ /** Remove the mapping for {@code key}, if present. */
+ void invalidate(@NotNull K key);
+
+ /** Remove all mappings from the cache. */
+ void invalidateAll();
+
+ /**
+ * Remove all mappings whose keys are in {@code keys}.
+ *
+ * Note: no Oak module currently calls this method. It can be removed
+ * from the interface if deemed unnecessary.
+ */
+ void invalidateAll(@NotNull Iterable extends K> keys);
+
+ /**
+ * Return the approximate number of entries in the cache.
+ *
+ * Note: no Oak module currently calls this method directly
+ * ({@code asMap().size()} is used instead). It can be removed from the
+ * interface if deemed unnecessary.
+ */
+ long estimatedSize();
+
+ /** Return a snapshot of this cache's cumulative statistics. */
+ @NotNull
+ CacheStatsSnapshot stats();
+
+ /** Return a concurrent, live view of the cache entries. */
+ @NotNull
+ ConcurrentMap asMap();
+
+ /**
+ * Return all cached values for the given keys (cache hits only).
+ *
+ * Note: no Oak module currently calls this method (CacheLIRS throws
+ * {@code UnsupportedOperationException} for it). It can be removed from the
+ * interface if deemed unnecessary.
+ */
+ @NotNull
+ Map getAllPresent(@NotNull Iterable extends K> keys);
+
+ /**
+ * Clean up expired or otherwise reclaimable entries.
+ *
+ * Note: no Oak module currently calls this method (CacheLIRS
+ * implementation is a no-op). It can be removed from the interface if
+ * deemed unnecessary.
+ */
+ void cleanUp();
+}
+```
+
+**`put(K, V, int memory)` (CacheLIRS-specific weighted put):** `DefaultSegmentWriter` in
+`oak-segment-tar` calls `nodeCache.put(key, value, memoryCost)`. This is a CacheLIRS internal
+that has no equivalent in the Caffeine `Cache` interface — Caffeine derives weight from the
+configured `Weigher` at insertion time. **Migration for this call site:** replace with
+`Cache.put(key, value)` and ensure an `Weigher` is configured on the builder. The
+weigher receives the key and value at insertion time, making per-call weight unnecessary.
+
+#### `LoadingCache` extends `Cache`
+
+Used by `ElasticIndexStatistics` (loading cache with refresh semantics).
+
+```java
+/**
+ * A cache that automatically loads absent entries from a pre-configured
+ * {@link CacheLoader}.
+ *
+ * {@code get(K)} follows Caffeine's loading-cache contract: runtime exceptions
+ * propagate directly and checked loader failures are wrapped in
+ * {@link CompletionException}. {@code refresh(K)} returns a future representing
+ * the refresh work.
+ */
+@ProviderType
+public interface LoadingCache extends Cache {
+
+ /**
+ * Return the cached value, loading it via the pre-configured loader if absent.
+ *
+ */
+ @NotNull
+ V get(@NotNull K key);
+
+ /**
+ * Trigger a reload of the value for {@code key}. The stale value remains
+ * available until the reload completes. The returned future matches
+ * Caffeine's refresh contract; the LIRS adapter completes it after its
+ * best-effort synchronous refresh path runs.
+ */
+ CompletableFuture refresh(@NotNull K key);
+}
+```
+
+#### `Weigher`
+
+```java
+/**
+ * Determines the weight of a cache entry. Replaces direct use of Caffeine's
+ * {@code Weigher} and Guava's {@code Weigher} in consumer modules.
+ */
+@FunctionalInterface
+public interface Weigher {
+
+ /**
+ * Return the weight of the entry. The unit is typically bytes but is
+ * cache-specific. Must be non-negative.
+ */
+ int weigh(@NotNull K key, @NotNull V value);
+}
+```
+
+> **Decision — `EmpiricalWeigher`:** `EmpiricalWeigher` currently implements
+> `org.apache.jackrabbit.guava.common.cache.Weigher` (Guava shim).
+> It will be changed to implement `Weigher` instead. The adapter
+> layer inside `oak-core-spi` wraps `Weigher` into the backend-specific weigher type
+> (`Guava Weigher` for `LirsCacheAdapter`, `Caffeine Weigher` for `CaffeineCacheAdapter`).
+> `EmpiricalWeigher` remains public because existing callers reference it. A temporary
+> compatibility base keeps it assignable to the Guava shim `Weigher` until Final Cleanup.
+
+#### `EvictionCause` (enum)
+
+```java
+/**
+ * Reason an entry was removed from the cache. Mirrors the common subset of
+ * Caffeine's and CacheLIRS's removal causes without exposing either.
+ */
+public enum EvictionCause {
+ /** Manually invalidated. */
+ EXPLICIT,
+ /** Replaced by a new value for the same key. */
+ REPLACED,
+ /** Evicted due to size/weight constraint. */
+ SIZE,
+ /** Expired. */
+ EXPIRED,
+ /** Collected (weak/soft reference reclaimed). */
+ COLLECTED
+}
+```
+
+#### `EvictionListener`
+
+```java
+/**
+ * Callback invoked when an entry is removed from the cache.
+ *
+ * Warning: it is unsafe to call cache methods from within the listener.
+ * Some implementations hold internal locks during the callback.
+ */
+@FunctionalInterface
+public interface EvictionListener {
+
+ void onEviction(@NotNull K key, @Nullable V value, @NotNull EvictionCause cause);
+}
+```
+
+#### `CacheBuilder`
+
+`CacheBuilder` is the Oak API entry point for creating Caffeine-backed caches. It no
+longer switches between backends. Call sites that still require loading `CacheLIRS`
+keep using `CacheLIRS.newBuilder()` and expose the resulting cache through
+`CacheLIRS.asOakCache()`.
+
+```java
+/**
+ * Fluent builder for Caffeine-backed {@link Cache} instances.
+ *
+ * Usage:
+ *
+ * Cache<String, byte[]> cache = CacheBuilder.<String, byte[]>newBuilder()
+ * .maximumWeight(64 * 1024 * 1024)
+ * .weigher((k, v) -> v.length)
+ * .removalListener((k, v, cause) -> LOG.info("evicted {}", k))
+ * .recordStats()
+ * .build();
+ *
+ */
+public final class CacheBuilder {
+
+ // --- Builder state ---
+ private String module;
+ private long maximumWeight = -1;
+ private long maximumSize = -1;
+ private Weigher weigher;
+ private EvictionListener removalListener;
+ private boolean recordStats;
+ private Duration expireAfterAccess;
+ private Duration expireAfterWrite;
+ private Duration refreshAfterWrite; // Caffeine-only (loading caches)
+
+ public static CacheBuilder newBuilder() { ... }
+
+ public CacheBuilder maximumWeight(long maximumWeight) { ... }
+ public CacheBuilder maximumSize(long maximumSize) { ... }
+ public CacheBuilder weigher(Weigher weigher) { ... }
+ public CacheBuilder removalListener(EvictionListener listener) { ... }
+ public CacheBuilder recordStats() { ... }
+ public CacheBuilder expireAfterAccess(Duration duration) { ... }
+ public CacheBuilder expireAfterWrite(Duration duration) { ... }
+ public CacheBuilder refreshAfterWrite(Duration duration) { ... }
+ public CacheBuilder ticker(Supplier ticker) { ... }
+ public CacheBuilder ticker(Clock clock) { ... }
+
+ /**
+ * Build a non-loading cache.
+ */
+ public Cache build() { ... }
+
+ /**
+ * Build a loading cache with the given loader.
+ * The loader is key-aware and may throw a checked exception (see {@link CacheLoader}).
+ */
+ public LoadingCache build(CacheLoader loader) { ... }
+}
+```
+
+### Stats: `CacheStatsSnapshot` value object (Batch 0) vs. `AbstractCacheStats` decoupling (Final Cleanup)
+
+On trunk, `AbstractCacheStats.getCurrentStats()` returns Guava shim `CacheStats`. There are
+**four subclasses** across two modules that override it:
+
+| Class | Module |
+|-------|--------|
+| `CacheStats` | `oak-core-spi` |
+| `RecordCacheStats` | `oak-segment-tar` |
+| `SegmentCache.Stats` (inner class) | `oak-segment-tar` |
+| `SegmentCacheStats` | `oak-segment-tar` |
+
+Changing `AbstractCacheStats.getCurrentStats()` in Batch 0 would break the three
+`oak-segment-tar` subclasses immediately — they are not migrated until Batches 7/8.
+Therefore the `AbstractCacheStats` return-type change is **deferred to Final Cleanup**,
+when all subclasses have already been updated.
+
+In Batch 0 we only:
+
+1. **Add `CacheStatsSnapshot`** — an Oak-owned stats snapshot class (immutable value object) with
+ the same fields as Guava's `CacheStats` (`hitCount`, `missCount`, `loadSuccessCount`,
+ `loadFailureCount`, `totalLoadTime`, `evictionCount`). Includes `minus(CacheStatsSnapshot)`,
+ `requestCount()`, `hitRate()`, `missRate()`, etc. `AbstractCacheStats` is **not changed**.
+
+2. **Add `CacheStatsAdapter`** (package-private) — extends `AbstractCacheStats` and wraps
+ an `Cache`. Its `getCurrentStats()` still returns Guava shim `CacheStats` (converted
+ from the `Cache`'s `CacheStatsSnapshot`) so it compiles against the unchanged
+ `AbstractCacheStats`. This is the stats handle returned by `CacheBuilder` and used by
+ consumer modules after migration.
+
+Consumer modules that currently call `new CacheStats(guavaCache, ...)` switch to
+`CacheStatsAdapter` as part of their individual migration task. The existing `CacheStats`
+class and `AbstractCacheStats` are left untouched until Final Cleanup.
+
+**In each per-module migration task** (Batches 1–9), if the module has a custom
+`AbstractCacheStats` subclass (e.g. `RecordCacheStats`), that subclass is updated to
+obtain stats from the new `Cache` and convert them to Guava `CacheStats` inside
+`getCurrentStats()`. The return type stays Guava until Final Cleanup.
+
+**In Final Cleanup** (Batch 10), once every subclass has been updated:
+- `AbstractCacheStats.getCurrentStats()` return type changes from Guava `CacheStats` to
+ `CacheStatsSnapshot`; internal `lastSnapshot` field and `stats()` method rewritten accordingly
+- `CacheStats.getCurrentStats()` updated to return `CacheStatsSnapshot` (converting from Guava)
+- All other subclasses drop their Guava conversion shim — they already have `CacheStatsSnapshot`
+
+### Hidden implementations — packages `org.apache.jackrabbit.oak.cache.impl.lirs` and `...caffeine` (package-private)
+
+#### `LirsCacheAdapter` + `LirsLoadingCacheAdapter`
+
+- `LirsCacheAdapter` wraps the existing `CacheLIRS` for manual caches.
+- `LirsLoadingCacheAdapter` wraps the loading `CacheLIRS` path for `LoadingCache`.
+- `CacheLIRS.asOakCache()` exposes loading `CacheLIRS` instances through the Oak
+ `LoadingCache` API.
+- Delegates all operations to `CacheLIRS`.
+- Adapts `Weigher` to `org.apache.jackrabbit.guava.common.cache.Weigher` (the Guava shim
+ interface that `CacheLIRS` currently uses).
+- Adapts `EvictionListener` / `EvictionCause` to `CacheLIRS.EvictionCallback` /
+ `org.apache.jackrabbit.guava.common.cache.RemovalCause`.
+- **Exception exposure — `get(K, Function)`:** adapts the key-aware mapping function to
+ `CacheLIRS.get(K, Callable)` and converts checked failures to `CompletionException`
+ while propagating runtime failures directly.
+- **Exception exposure — `LoadingCache.get(K)`:** delegates to `CacheLIRS.get(K)` and
+ converts checked failures to `CompletionException` while propagating runtime failures directly.
+- **Refresh exposure — `LoadingCache.refresh(K)`:** runs CacheLIRS refresh and returns a
+ completed future representing the best-effort synchronous refresh result.
+- `CacheLIRS` now exposes `asOakCache()` for instances created with a loader while
+ remaining a concrete class implementing the Guava shim `LoadingCache` for internal use only.
+ Its Guava dependency is fully hidden behind the Oak `LoadingCache` view returned by
+ `asOakCache()`.
+
+#### `CaffeineCacheAdapter` + `CaffeineLoadingCacheAdapter`
+
+- `CaffeineCacheAdapter` wraps a `com.github.benmanes.caffeine.cache.Cache` for manual caches.
+- `CaffeineLoadingCacheAdapter` wraps a `LoadingCache` for `LoadingCache`.
+- Delegates all operations to Caffeine.
+- **`get(K, Function)`** delegates directly to Caffeine's manual-cache API.
+- **`LoadingCache.get(K)`** delegates directly to Caffeine's loading-cache API.
+- **`LoadingCache.refresh(K)`** delegates directly to Caffeine and returns the refresh future.
+- Adapts `Weigher` to `com.github.benmanes.caffeine.cache.Weigher`.
+- Adapts `EvictionListener` / `EvictionCause` to Caffeine `RemovalListener` / `RemovalCause`.
+- `build()` returns `CaffeineCacheAdapter`; `build(CacheLoader)` returns
+ `CaffeineLoadingCacheAdapter`. Manual caches do not implement `LoadingCache`.
+
+#### Builder validation rules
+
+`CacheBuilder` validates configuration before constructing the Caffeine adapter.
+
+- Exactly one of `maximumSize` or `maximumWeight` must be configured.
+- `maximumWeight` requires `weigher(...)`.
+- `weigher(...)` requires `maximumWeight(...)`.
+- `refreshAfterWrite(...)` is valid only for `build(CacheLoader)`.
+
+### OSGi changes
+
+- `oak-core-spi` exports both `org.apache.jackrabbit.oak.cache` (existing) and the new
+ `org.apache.jackrabbit.oak.cache.api` package. The public API types, including
+ `CacheBuilder` and `CacheStatsAdapter`, live under
+ `org.apache.jackrabbit.oak.cache.api`. The hidden adapter sub-packages
+ (`org.apache.jackrabbit.oak.cache.impl.lirs`, `org.apache.jackrabbit.oak.cache.impl.caffeine`)
+ are not re-exported.
+- On trunk, `oak-core-spi` has no Caffeine dependency. Batch 0 adds Caffeine as a compile
+ dependency of `oak-core-spi` (confined to the hidden `CaffeineCacheAdapter`). Consumer bundles
+ do **not** need to import Caffeine packages once they migrate to the Oak cache API.
+
+### Files changed
+
+| File | Change |
+|------|--------|
+| `oak-core-spi/.../cache/api/Cache.java` | **New** (`get(K, Function)` — matches Caffeine's manual-cache contract) |
+| `oak-core-spi/.../cache/api/LoadingCache.java` | **New** (`get(K)` unchecked, `refresh(K)` returns `CompletableFuture`) |
+| `oak-core-spi/.../cache/api/CacheLoader.java` | **New** (functional interface; `V load(K) throws Exception`) |
+| `oak-core-spi/.../cache/api/Weigher.java` | **New** |
+| `oak-core-spi/.../cache/api/EvictionCause.java` | **New** |
+| `oak-core-spi/.../cache/api/EvictionListener.java` | **New** (`void onEviction(K, V, EvictionCause)`) |
+| `oak-core-spi/.../cache/api/CacheStatsSnapshot.java` | **New** (immutable stats snapshot record) |
+| `oak-core-spi/.../cache/api/CacheBuilder.java` | **New** (public final class; Caffeine-backed cache builder) |
+| `oak-core-spi/.../cache/api/CacheStatsAdapter.java` | **New** (package-private; extends `AbstractCacheStats`; `getCurrentStats()` returns Guava shim `CacheStats` converted from `CacheStatsSnapshot` — Guava return type preserved until Final Cleanup) |
+| `oak-core-spi/.../cache/impl/lirs/LirsCacheAdapter.java` | **New** (package-private) |
+| `oak-core-spi/.../cache/impl/lirs/LirsLoadingCacheAdapter.java` | **New** (package-private) |
+| `oak-core-spi/.../cache/impl/caffeine/CaffeineCacheAdapter.java` | **New** (package-private) |
+| `oak-core-spi/.../cache/impl/caffeine/CaffeineLoadingCacheAdapter.java` | **New** (package-private) |
+| `oak-core-spi/.../cache/EmpiricalWeigher.java` | Modify: implement `Weigher` (Oak API) with temporary Guava-shim compatibility bridge |
+| `oak-core-spi/.../cache/AbstractCacheStats.java` | **No change** (deferred to Final Cleanup) |
+| `oak-core-spi/.../cache/CacheStats.java` | **No change** (existing Guava-wrapping JMX class; deferred to Final Cleanup) |
+| `oak-core-spi/.../cache/CacheLIRS.java` | **No change** (remains `@Internal`) |
+
+### Acceptance criteria
+
+- `oak-core-spi` compiles with no new Caffeine or Guava types in the public API surface.
+- All existing `oak-core-spi` tests pass (`CacheTest`, `CacheSizeTest`, `ConcurrentTest`,
+ `ConcurrentPerformanceTest`).
+- New focused tests cover the new Oak-facing adapters and builder:
+ `CacheBuilderTest`, `CaffeineCacheAdapterTest`, `LirsCacheAdapterTest`,
+ `LirsLoadingCacheAdapterTest`, and `CacheLIRSOakAdapterTest`.
+- No existing consumer module is changed in this batch — all current `CacheLIRS.newBuilder()`
+ and direct `Caffeine.newBuilder()` call sites still compile.
+
+---
+
+## Batch 1 — `oak-blob-cloud`
+
+### Prerequisite
+Batch 0 merged.
+
+### Scope
+Migrate `S3Backend` from direct Guava-shim cache (`CacheBuilder.newBuilder()`) to `CacheBuilder`.
+
+On trunk, `S3Backend` uses:
+- `org.apache.jackrabbit.guava.common.cache.Cache`
+- `org.apache.jackrabbit.guava.common.cache.CacheBuilder`
+
+### Files changed
+
+| File | Change |
+|------|--------|
+| `oak-blob-cloud/.../s3/S3Backend.java` | Replace `CacheBuilder.newBuilder()...build()` with `CacheBuilder.newBuilder()...build()`. Replace `Cache` field with `Cache`. Remove Guava shim cache imports. |
+
+### Acceptance criteria
+
+- No `org.apache.jackrabbit.guava.common.cache` imports in `oak-blob-cloud/src/` (main and test).
+- `S3Backend` cache tests pass (strengthen before migration).
+- Presigned URI caching semantics preserved (expiry, max-size).
+
+---
+
+## Batch 2 — `oak-blob-cloud-azure`
+
+### Prerequisite
+Batch 0 merged.
+
+### Scope
+Migrate `AzureBlobStoreBackend` and `AzureBlobStoreBackendV8` from direct Guava-shim cache
+to `CacheBuilder`.
+
+On trunk, both backends use:
+- `org.apache.jackrabbit.guava.common.cache.Cache`
+- `org.apache.jackrabbit.guava.common.cache.CacheBuilder`
+
+### Files changed
+
+| File | Change |
+|------|--------|
+| `oak-blob-cloud-azure/.../AzureBlobStoreBackend.java` | Replace `CacheBuilder.newBuilder()` with `CacheBuilder`. Replace `Cache` with `Cache`. Remove Guava shim cache imports. |
+| `oak-blob-cloud-azure/.../v8/AzureBlobStoreBackendV8.java` | Same. |
+
+### Acceptance criteria
+
+- No `org.apache.jackrabbit.guava.common.cache` imports in `oak-blob-cloud-azure/src/` (main and test).
+- Azure backend cache tests pass (strengthen before migration).
+
+---
+
+## Batch 3 — `oak-blob`
+
+### Prerequisite
+Batch 0 merged.
+
+### Scope
+Migrate `BlobIdSet` from direct Guava-shim cache to `CacheBuilder`.
+
+On trunk, `BlobIdSet` uses `org.apache.jackrabbit.guava.common.cache.Cache` and
+`org.apache.jackrabbit.guava.common.cache.CacheBuilder`.
+
+### Files changed
+
+| File | Change |
+|------|--------|
+| `oak-blob/.../split/BlobIdSet.java` | Replace `CacheBuilder.newBuilder()` with `CacheBuilder`. Replace `Cache` with `Cache`. Remove Guava shim cache imports. |
+
+### Acceptance criteria
+
+- No `org.apache.jackrabbit.guava.common.cache` imports in `oak-blob/src/` (main and test).
+- `BlobIdSet` membership and bounded-cache tests pass.
+
+---
+
+## Batch 4 — `oak-search-elastic`
+
+### Prerequisite
+Batch 0 merged.
+
+### Scope
+Migrate `ElasticIndexStatistics` from Guava-shim `LoadingCache` to `LoadingCache` via
+`CacheBuilder.build(loader)`.
+
+On trunk, `ElasticIndexStatistics` uses:
+- `org.apache.jackrabbit.guava.common.cache.CacheBuilder`
+- `org.apache.jackrabbit.guava.common.cache.CacheLoader`
+- `org.apache.jackrabbit.guava.common.cache.LoadingCache`
+
+### Files changed
+
+| File | Change |
+|------|--------|
+| `oak-search-elastic/.../ElasticIndexStatistics.java` | Replace `CacheBuilder.newBuilder()` with `CacheBuilder`. Replace `LoadingCache` fields with `LoadingCache`. Replace `CacheLoader` with `CacheLoader` (key-aware, throws checked exception) passed to `CacheBuilder.build(loader)`. Remove Guava shim cache imports. |
+
+### Acceptance criteria
+
+- No `org.apache.jackrabbit.guava.common.cache` imports in `oak-search-elastic/src/` (main and test).
+- `ElasticIndexStatisticsTest` passes covering expiry, refresh, and failure behavior.
+
+### Ticker support
+`ElasticIndexStatistics` uses a controllable clock for deterministic time in tests.
+`CacheBuilder` exposes two overloads:
+- `ticker(Supplier ticker)` — raw nanosecond supplier, for custom time sources
+- `ticker(Clock clock)` — convenience overload wrapping `java.time.Clock`; internally delegates to `ticker(() -> TimeUnit.MILLISECONDS.toNanos(clock.millis()))`
+
+---
+
+## Batch 5 — `oak-search`
+
+### Prerequisite
+Batch 0 merged.
+
+### Scope
+Migrate `ExtractedTextCache` from Guava-shim cache + Oak `CacheStats` (which wraps a Guava
+`Cache`) to `CacheBuilder` + `CacheStatsAdapter`.
+
+On trunk, `ExtractedTextCache` uses:
+- `org.apache.jackrabbit.guava.common.cache.Cache`
+- `org.apache.jackrabbit.guava.common.cache.CacheBuilder`
+- `org.apache.jackrabbit.guava.common.cache.Weigher` (local `EmpiricalWeigher` inner class)
+- `org.apache.jackrabbit.oak.cache.CacheStats` (passing a Guava `Cache` to its constructor)
+
+### Files changed
+
+| File | Change |
+|------|--------|
+| `oak-search/.../ExtractedTextCache.java` | Replace `CacheBuilder.newBuilder()` with `CacheBuilder`. Replace `Cache` with `Cache`. Replace the local `EmpiricalWeigher` inner class (implements Guava `Weigher`) with an `Weigher` lambda. Replace `new CacheStats(guavaCache, ...)` with `CacheStatsAdapter` obtained from the builder. Remove all Guava shim cache imports. |
+| `oak-lucene/.../LuceneIndexProviderService.java` | `getCacheStats()` return type cascade: `CacheStats` → `AbstractCacheStats`. |
+| `oak-run-commons/.../DocumentStoreIndexerBase.java` | `getCacheStats()` return type cascade: `CacheStats` → `AbstractCacheStats`. |
+| `oak-lucene/pom.xml` | Add `org/apache/jackrabbit/oak/cache/api/CacheStatsAdapter.class` to the `Embed-Dependency` inline list for `oak-core-spi`. Without this, `ExtractedTextCache` (inlined in oak-lucene) assigns a `CacheStatsAdapter` instance — whose superclass `AbstractCacheStats` is resolved from `oak-core-spi` bundle classloader — to a field declared as `AbstractCacheStats` from oak-lucene's own inlined copy; the two classes are distinct at JVM verification time, causing a `VerifyError` in `IndexVersionSelectionIT`. Known limitation: `org.apache.jackrabbit.oak.cache.api` is still absent from `Import-Package` (bnd split-package heuristic); `maxWeight > 0` usage in OSGi requires OAK-3598. |
+
+### Acceptance criteria
+
+- No `org.apache.jackrabbit.guava.common.cache` imports in `oak-search/src/` (main and test).
+- `ExtractedTextCache` cache tests pass, including stats reporting.
+- `DocumentStoreIndexerBase` compiles cleanly in `oak-run-commons`.
+
+---
+
+## Batch 6 — `oak-store-document`
+
+### Prerequisite
+Batch 0 merged.
+
+### Scope
+This is the largest and most complex migration. `oak-store-document` has 38 files referencing
+cache types. The key changes:
+
+1. **`DocumentNodeStoreBuilder.buildCache()`** — replace the `CacheLIRS.newBuilder()` call
+ with a single `CacheBuilder` call for Caffeine-backed caches. On trunk there is no
+ `oak.documentMK.lirsCache` toggle to remove (that was introduced only in the rejected
+ PR #2807).
+
+2. **All `Cache` fields and parameters** across the module become `Cache`.
+
+3. **`EvictionListener`** — replace `org.apache.jackrabbit.guava.common.cache.RemovalCause`
+ (Guava shim, used on trunk) with `EvictionCause`. The module's own `EvictionListener`
+ interface changes its signature from `evicted(K, V, RemovalCause)` to
+ `evicted(K, V, EvictionCause)`.
+
+4. **`ForwardingListener`** — update to use `EvictionListener`.
+
+5. **`CacheStats` construction sites** — replace with stats obtained from the `Cache`
+ (via `CacheStatsAdapter`).
+
+6. **`Weigher` references** — replace Caffeine `Weigher` with `Weigher` where the weigher
+ is configured.
+
+7. **Persistent cache integration** (`PersistentCache`, `NodeCache`) — these wrap an
+ underlying cache. They must be updated to accept `Cache` instead of `Cache`.
+
+8. **`asMap()` usage** in `NodeDocumentCache.keys()` and `NodeDocumentCache.values()` —
+ `Cache.asMap()` provides this.
+
+### Files changed
+
+| File | Change |
+|------|--------|
+| `DocumentNodeStoreBuilder.java` | Replace `CacheLIRS.newBuilder()` calls with `CacheBuilder`. |
+| `DocumentNodeStore.java` | Change `Cache` types to `Cache`. |
+| `NodeDocument.java` | Update cache type references. |
+| `cache/NodeDocumentCache.java` | `Cache` → `Cache`. `asMap()` calls stay (method exists on `Cache`). |
+| `cache/ForwardingListener.java` | `RemovalCause` → `EvictionCause`. |
+| `persistentCache/EvictionListener.java` | `RemovalCause` → `EvictionCause`. |
+| `persistentCache/NodeCache.java` | `Cache` → `Cache`. |
+| `persistentCache/PersistentCache.java` | `Cache` → `Cache`. |
+| `MemoryDiffCache.java` | `Cache` → `Cache`. |
+| `LocalDiffCache.java` | `Cache` → `Cache`. |
+| `TieredDiffCache.java` | `Cache` → `Cache`. |
+| `CachingCommitValueResolver.java` | `Cache` → `Cache`. |
+| `MongoDocumentStore.java` | Remove direct Caffeine/Guava cache references. |
+| `RDBDocumentStore.java` | Remove direct Caffeine/Guava cache references. |
+| `MemoryDocumentStore.java` | Update if it references cache types. |
+| `JournalDiffLoader.java` | Update if it references cache types. |
+| Various `util/` classes | Update `CacheValue` usage (no change needed if they only reference the interface). |
+| `BranchTest.java` | `Cache` import: Guava shim → Oak API. |
+| Various test classes | Update to use `Cache` types. |
+
+### Acceptance criteria
+
+- No `org.apache.jackrabbit.guava.common.cache` imports in `oak-store-document/src/` (main and test).
+- Persistent cache eviction behavior unchanged.
+- Checked loader failures surface as `CompletionException` on the Oak-visible API.
+- Synchronous eviction callback timing preserved where persistent cache depends on it.
+- All existing tests pass: `NodeDocumentCacheTest`, `CacheChangesTrackerTest`,
+ `AsyncCacheTest`, `DisableCacheTest`, `BranchTest`, `MemoryDiffCacheTest`, `LocalDiffCacheTest`,
+ `persistentCache.CacheTest`, `persistentCache.NodeCacheTest`.
+
+---
+
+## Batch 7 — `oak-segment-tar`
+
+### Prerequisite
+Batch 0 merged.
+
+### Scope
+
+`oak-segment-tar` has three distinct cache subsystems. On trunk all use the Guava shim
+(`org.apache.jackrabbit.guava.common.cache.*`) or `CacheLIRS`:
+
+1. **`SegmentCache`** — uses Guava-shim `CacheBuilder.newBuilder()` with a custom
+ `RemovalListener` and manual stats tracking. Migrate to `CacheBuilder`.
+
+2. **`RecordCache`** — uses Guava-shim `CacheBuilder.newBuilder()` with a `Weigher`.
+ Migrate to `CacheBuilder`.
+
+3. **`ReaderCache`** — uses `CacheLIRS.newBuilder()` directly. Migrate to
+ `CacheBuilder` (which creates a Caffeine-backed `Cache`).
+
+4. **`PriorityCache`** — check trunk; update if it references Guava shim cache types directly.
+
+5. **`WriterCacheManager`** — update if it references Guava shim cache builder types.
+
+6. **`RecordCacheStats`**, **`SegmentCache.Stats`**, **`SegmentCacheStats`** — these are
+ `AbstractCacheStats` subclasses. In this batch they are updated to obtain stats from the
+ migrated `Cache`, converting `CacheStatsSnapshot` → Guava shim `CacheStats` inside
+ `getCurrentStats()`. The Guava return type is kept until Final Cleanup changes the base
+ class.
+
+### Files changed
+
+| File | Change |
+|------|--------|
+| `SegmentCache.java` | `CacheBuilder.newBuilder()` (Guava shim) → `CacheBuilder`. `Cache` → `Cache`. Guava `RemovalCause` → `EvictionCause`. |
+| `RecordCache.java` | `CacheBuilder.newBuilder()` (Guava shim) → `CacheBuilder`. `Cache` → `Cache`. Guava `Weigher` → `Weigher`. |
+| `ReaderCache.java` | `CacheLIRS.newBuilder()` → `CacheBuilder`. `CacheLIRS` → `Cache`. |
+| `PriorityCache.java` | Update if it references Caffeine types directly. |
+| `WriterCacheManager.java` | Update cache type references. |
+| `RecordCacheStats.java` | Update to use `CacheStatsSnapshot`. |
+| `CacheWeights.java` | `Weigher` → `Weigher`. |
+| `spi/persistence/persistentcache/SegmentCacheStats.java` | Update. |
+| `CachingSegmentReader.java` | Update cache type references. |
+| `SegmentNodeStoreRegistrar.java` | Update if it references cache builder types. |
+
+### Acceptance criteria
+
+- No `org.apache.jackrabbit.guava.common.cache` imports in `oak-segment-tar/src/` (main and test).
+- `SegmentCacheTest`, `PriorityCacheTest`, `ConcurrentPriorityCacheTest` pass.
+- Segment unload timing and memoization behavior unchanged.
+- Eviction callback timing unchanged.
+
+---
+
+## Batch 8 — `oak-blob-plugins`
+
+### Prerequisite
+Batch 0 merged.
+
+### Scope
+
+`oak-blob-plugins` mixes `CacheLIRS.newBuilder()`, Guava-shim `CacheBuilder.newBuilder()`,
+and Oak `CacheStats` (which wraps a Guava shim `Cache`). All paths migrate to `CacheBuilder`
++ `Cache`.
+
+### Files changed
+
+| File | Change |
+|------|--------|
+| `FileCache.java` | `CacheLIRS.newBuilder()` → `CacheBuilder`. `Cache` → `Cache`. `CacheLIRS.EvictionCallback` → `EvictionListener`. `CacheStatsSnapshot` → stats from `CacheBuilder`. |
+| `UploadStagingCache.java` | Update cache types. |
+| `CompositeDataStoreCache.java` | Update cache types. |
+| `AbstractSharedCachingDataStore.java` | Update cache types. |
+| `CachingBlobStore.java` | Update cache types. |
+| `DataStoreCacheUpgradeUtils.java` | Update cache types. |
+
+### Acceptance criteria
+
+- No `org.apache.jackrabbit.guava.common.cache` imports in `oak-blob-plugins/src/` (main and test).
+- No `CacheLIRS` references in `oak-blob-plugins/src/` (main and test).
+- File cache eviction/deletion behavior unchanged.
+- All blob-plugins cache tests pass.
+
+---
+
+## Batch 9 — `oak-run-commons` and remaining modules
+
+### Prerequisite
+Batch 0 merged.
+
+### Scope
+
+- `DocumentNodeStoreHelper` — update any `CacheLIRS` or Caffeine references to `Cache`.
+- `DocumentStoreIndexerBase` — update if it references cache types.
+- Scan for any other modules with residual Caffeine/Guava cache imports and migrate them.
+
+### Files changed
+
+| File | Change |
+|------|--------|
+| `oak-run-commons/.../DocumentNodeStoreHelper.java` | `CacheLIRS` → `Cache`. |
+| `oak-run-commons/.../DocumentStoreIndexerBase.java` | Update if needed. |
+
+### Acceptance criteria
+
+- No direct Caffeine or Guava cache usage in any module's `src/` (main or test) except
+ `oak-core-spi` (where the adapters live).
+
+---
+
+## Batch 10 — Final Cleanup
+
+### Prerequisite
+All previous batches merged.
+
+### Scope
+
+1. **Decouple `AbstractCacheStats` from Guava shim.** All `AbstractCacheStats` subclasses
+ have been updated by their module tasks and already produce `CacheStatsSnapshot` internally.
+ Now make the base class match:
+ - Change `AbstractCacheStats.getCurrentStats()` return type from Guava shim `CacheStats`
+ to `CacheStatsSnapshot`
+ - Rewrite internal `lastSnapshot` field and `stats()` method to use `CacheStatsSnapshot`
+ - Update `CacheStats.getCurrentStats()` to return `CacheStatsSnapshot` (converting from its
+ Guava snapshot)
+ - Update `CacheStatsAdapter.getCurrentStats()` to return `CacheStatsSnapshot` directly
+ (drop the Guava conversion shim it carried since Batch 0)
+ - Remove the temporary Guava-compatibility bridge
+ (`GuavaCompatibleEmpiricalWeigher`) if it is still present
+ - Update `RecordCacheStats`, `SegmentCache.Stats`, `SegmentCacheStats` to return
+ `CacheStatsSnapshot` directly (drop their Guava conversion shims)
+
+2. **Verify isolation.** Grep the entire codebase: no module outside `oak-core-spi`
+ imports `com.github.benmanes.caffeine.cache` or `org.apache.jackrabbit.guava.common.cache`.
+
+3. **Remove CacheLIRS and the LIRS fallback.** With all modules on `Cache` API and
+ Caffeine validated as the production backend:
+ - Remove `LirsCacheAdapter` entirely
+ - Remove `CacheLIRS` (or mark it `@Deprecated(forRemoval = true)` if a deprecation
+ cycle is preferred — but since it was already `@Internal`, outright removal is acceptable)
+ - Remove any remaining `CacheLIRS.asOakCache()` bridge usage from migrated modules
+
+4. **Mark the old `CacheStats` class** (the one accepting a Guava shim `Cache,?>` in its
+ constructor) as `@Deprecated(forRemoval = true)`. Consumers should use
+ `CacheStatsAdapter` obtained from the builder.
+
+5. **Remove Caffeine from OSGi `Import-Package`** of consumer bundles. Only `oak-core-spi`
+ should import Caffeine packages.
+
+6. **Remove any Guava cache shim re-exports** if they still exist.
+
+### Acceptance criteria
+
+- `mvn clean install -DskipTests` succeeds.
+- `mvn clean install` succeeds (full test suite).
+- No Caffeine or Guava cache types in any public API surface outside `oak-core-spi`.
+- No `CacheLIRS` or `LirsCacheAdapter` classes remain (or `CacheLIRS` is `@Deprecated(forRemoval = true)`).
+- `CacheBuilder` has no `lirs` code path — Caffeine is the sole implementation.
+- OSGi integration tests pass (`oak-it-osgi`).
+
+---
+
+## Test Plan
+
+Every batch follows this sequence:
+
+1. **Before migration:** Add or strengthen unit tests covering the existing cache behavior
+ in the target module. Run them against the unmodified code to establish a baseline.
+2. **Migrate the implementation.**
+3. **After migration:** Re-run the same tests. They must pass with identical behavior.
+4. **OSGi check:** Inspect generated bundle manifests — consumer bundles must not import
+ Caffeine packages after migration.
+
+### Per-batch test focus
+
+| Batch | Key tests |
+|-------|-----------|
+| 0 | `CacheTest`, `CacheSizeTest`, `ConcurrentTest`, `ConcurrentPerformanceTest`, **new** `CacheBuilderTest`, `CaffeineCacheAdapterTest`, `LirsCacheAdapterTest`, `LirsLoadingCacheAdapterTest`, `CacheLIRSOakAdapterTest` |
+| 1 | `S3Backend` cache tests (presigned URI caching, expiry, hit/miss) |
+| 2 | Azure backend cache tests |
+| 3 | `BlobIdSet` membership and bounded cache tests |
+| 4 | `ElasticIndexStatisticsTest` (expiry, refresh, loader failure) |
+| 5 | `ExtractedTextCache` tests (weighing, expiry, stats reporting) |
+| 6 | `NodeDocumentCacheTest`, `CacheChangesTrackerTest`, `AsyncCacheTest`, `DisableCacheTest`, `MemoryDiffCacheTest`, `LocalDiffCacheTest`, `persistentCache.CacheTest`, `persistentCache.NodeCacheTest` |
+| 7 | `SegmentCacheTest`, `PriorityCacheTest`, `ConcurrentPriorityCacheTest`, `ReaderCacheTest` |
+| 8 | `FileCache` tests, `UploadStagingCache` tests, `CompositeDataStoreCache` tests |
+| 9 | `DocumentNodeStoreHelper` tests |
+| 10 | Full build (`mvn clean install`), OSGi integration tests (`oak-it-osgi`) |
+
+---
+
+## Assumptions and Design Decisions
+
+### Why `Cache` instead of Caffeine directly?
+
+The previous approach (PR #2807 / OAK-11946) migrated modules from Guava types to Caffeine
+types. This creates a hard dependency on Caffeine's API surface across the entire codebase —
+exactly the same coupling problem we had with Guava. If Caffeine ever introduces breaking
+changes, or if Oak needs to switch to a different cache library, every module would require
+another mass migration.
+
+`Cache` is a thin, stable interface owned by Oak. The Caffeine dependency is confined to
+`oak-core-spi`'s hidden implementation package. Consumer modules depend only on Oak types.
+
+### Trunk baseline
+
+On trunk (`origin/trunk`), all cache usage goes through the `org.apache.jackrabbit.guava`
+shim, which wraps the actual Guava library. Specifically:
+- `CacheLIRS` implements `org.apache.jackrabbit.guava.common.cache.LoadingCache`
+- `AbstractCacheStats.getCurrentStats()` returns `org.apache.jackrabbit.guava.common.cache.CacheStats`
+- `CacheStats` wraps `org.apache.jackrabbit.guava.common.cache.Cache,?>`
+- `EmpiricalWeigher` implements `org.apache.jackrabbit.guava.common.cache.Weigher`
+
+No Caffeine dependency exists in `oak-core-spi` or any consumer module on trunk. Batch 0
+introduces Caffeine into `oak-core-spi` as a hidden implementation detail only.
+
+### How CacheLIRS is preserved during migration
+
+`CacheLIRS` is **not removed**. It remains the existing Guava-backed LIRS
+implementation in `oak-core-spi`. For loading caches, `CacheLIRS.asOakCache()` wraps it
+behind the Oak `LoadingCache` interface. Existing code that directly uses `CacheLIRS`
+continues to compile until the consumer module is migrated in a later batch.
+
+`CacheBuilder` is now Caffeine-only. LIRS call sites that still need Oak loading-cache API
+exposure use `CacheLIRS.asOakCache()` directly instead of routing through `CacheBuilder`.
+
+### OSGi / bundle implications
+
+- `oak-core-spi` exports `org.apache.jackrabbit.oak.cache`. All new interfaces live in this
+ package.
+- `oak-core-spi` imports `com.github.benmanes.caffeine.cache` (private implementation detail).
+- After migration, consumer bundles (e.g., `oak-store-document`) **stop** importing
+ `com.github.benmanes.caffeine.cache` because they only reference `Cache` types.
+- This is a net reduction in coupling and a simpler OSGi wiring graph.
+
+### `Cache` API — Oak-owned contract with backend bridging
+
+The `Cache` API follows Caffeine's retrieval contract. Backend differences are
+bridged inside the hidden adapters so the transitional LIRS fallback remains
+transparent to callers:
+
+| Concern | CacheLIRS (trunk) | Caffeine | `Cache` decision |
+|---------|-------------------|----------|---------------------|
+| On-demand loader signature | `Callable` (no key) | `Function` (key-aware) | `Function` — matches Caffeine's manual-cache contract |
+| Loader exception | `ExecutionException` (checked) | `CompletionException` (unchecked) | Runtime failures propagate directly; checked failures surface as `CompletionException` |
+| Loading cache loader | `CacheLoader.load(K)` (Guava shim) | `CacheLoader.load(K)` (Caffeine) | `CacheLoader.load(K) throws Exception` — key-aware, checked |
+| `LoadingCache.get(K)` exception | `ExecutionException` (checked) | `CompletionException` (unchecked) | Runtime failures propagate directly; checked failures surface as `CompletionException` |
+| Async refresh | Not supported | `refreshAfterWrite` + `refresh(K)` | `LoadingCache.refresh(K)` returns `CompletableFuture` + `CacheBuilder.refreshAfterWrite(Duration)` |
+| Expiry | Not supported | `expireAfterAccess`, `expireAfterWrite` | Supported by `CacheBuilder`; `LirsCacheAdapter` ignores these (passed only when wrapping existing LIRS instances) |
+| Eviction callback | `EvictionCallback` (key, value, cause) | `RemovalListener` (key, value, cause) | `EvictionListener.onEviction(key, value, EvictionCause)` |
+| Stats snapshot | Guava `CacheStats` | Caffeine `CacheStats` | `CacheStatsSnapshot` (Oak-owned, same fields) |
+
+`LirsCacheAdapter.get(K, Function)` adapts the key-aware mapping function to
+`CacheLIRS.get(K, Callable)` and converts checked failures to `CompletionException`.
+`CaffeineCacheAdapter.get(K, Function)` delegates directly to Caffeine.
+
+### Why not put the API in a new `oak-cache-api` module?
+
+Adding a new module increases the build graph complexity and requires every consumer to add a
+new dependency. The `oak-core-spi` module already serves as the SPI home for Oak's internal
+contracts, and it already exports the `org.apache.jackrabbit.oak.cache` package. Placing the
+new interfaces there is the simplest path with zero new module wiring.
+
+### `CacheValue` stays unchanged
+
+`CacheValue` is a simple interface (`int getMemory()`) with no Guava or Caffeine dependency.
+It stays as-is. Modules that use `CacheValue`-typed keys/values continue to work.
+
+### Batches 1-5 and 6-8 are independently orderable
+
+Batches 1-5 (simple modules) have no dependencies on each other — only on Batch 0. They can
+be merged in any order. Batches 6-8 (complex modules) also depend only on Batch 0 and can be
+done in parallel if staffing allows. The numbering reflects a suggested order from simplest to
+most complex.
diff --git a/specs/guava-cache-removal/SPEC.md b/specs/guava-cache-removal/SPEC.md
new file mode 100644
index 00000000000..578d9feb275
--- /dev/null
+++ b/specs/guava-cache-removal/SPEC.md
@@ -0,0 +1,26 @@
+# Oak Cache API
+
+This is part of the broader Guava removal effort tracked in [OAK-10685](https://issues.apache.org/jira/browse/OAK-10685).
+
+## Goals
+
+- Introduce a small Oak-owned cache API in `oak-core-spi` that mirrors Caffeine's contract but contains no Caffeine-specific types in the public interfaces.
+- Consumers import only from `org.apache.jackrabbit.oak.cache.api`; the Caffeine-backed implementation stays hidden behind package-private adapters.
+- Swapping the underlying cache implementation must not require touching any consumer code.
+
+## Design Decisions
+
+- CacheLIRS is kept as-is for backward compatibility; it is not replaced.
+- `CacheBuilder` creates Caffeine-backed caches only; callers that still need CacheLIRS build it directly and expose it via `CacheLIRS.asOakCache()`.
+
+## Migration Constraints
+
+- Migrate one module per PR so each change is small, reviewable, and mergeable independently.
+- The `oak-core-spi` foundation must land first; after that all consumer modules can migrate in parallel.
+- Each module PR must pass the cache compatibility tests introduced in [OAK-12145](https://issues.apache.org/jira/browse/OAK-12145) / [PR #2811](https://github.com/apache/jackrabbit-oak/pull/2811).
+- Every PR must leave the codebase in a state that compiles and is realistically mergeable into Oak as-is; no PR may rely on a follow-up PR to restore buildability.
+- If a module cannot switch from Guava to Oak Cache in one PR without forcing simultaneous updates across many other modules, introduce an adapter to bridge the old and new cache contracts and use it to stage the migration incrementally.
+- When a method's return type changes as part of a migration, update all callers in all modules in the same PR; a missed caller compiles locally but breaks CI's full build.
+- Before declaring a PR done, grep the entire module (`src/main/java` and `src/test/java`) for `org.apache.jackrabbit.guava.common.cache`; test code must be migrated in the same PR as production code.
+- After migration, inspect the generated bundle manifests — consumer bundles must not import Caffeine packages.
+- Preserve behavioral equivalence: same eviction timing, same toggles, same observable cache semantics.
diff --git a/specs/guava-cache-removal/TASKS.md b/specs/guava-cache-removal/TASKS.md
new file mode 100644
index 00000000000..68b83f69a32
--- /dev/null
+++ b/specs/guava-cache-removal/TASKS.md
@@ -0,0 +1,541 @@
+# Oak Cache API Migration — Task Breakdown
+
+This document decomposes the Oak Cache API migration plan (PLAN.md) into independently mergeable JIRA-sized tasks. Each task produces one PR that compiles and passes tests without requiring any sibling task to be in-progress. Tasks are numbered sequentially (OAK-12147 through OAK-12162). Batch 0 splits into two tasks (API then implementations), Batch 6 splits into three (cache infra, diff caches, persistent cache), and Batch 7 splits into two (Guava-shim caches, CacheLIRS-based caches). All other batches map one-to-one.
+
+## Current local status
+
+- OAK-12147 API interfaces are implemented locally.
+- OAK-12148 hidden implementations and builder are implemented locally: `CacheBuilder` creates Caffeine-backed caches only; `CacheLIRS` instances are exposed via `CacheLIRS.asOakCache()`. Separate manual/loading adapters per backend, builder-side validation, and Javadocs are all in place.
+- OAK-12149 through OAK-12162 remain planning tasks in this document.
+
+## Dependency Graph
+
+```
+OAK-12147 (API interfaces)
+ |
+OAK-12148 (hidden impls + builder)
+ |
+ +--+--+--+--+--+--+--+--+--+--+--+
+ | | | | | | | | | | | |
+ 149 150 151 152 153 154 155 156 157 158 159 160
+ | | | | | | | | | | | |
+ +--+--+--+--+--+--+--+--+--+--+--+
+ |
+ OAK-12161
+ |
+ OAK-12162
+
+Parallel groups (all depend only on OAK-12148, can run concurrently):
+ OAK-12149 — oak-blob-cloud
+ OAK-12150 — oak-blob-cloud-azure
+ OAK-12151 — oak-blob
+ OAK-12152 — oak-search-elastic
+ OAK-12153 — oak-search
+ OAK-12154 — oak-store-document cache infra
+ OAK-12155 — oak-store-document diff caches
+ OAK-12156 — oak-store-document persistent cache
+ OAK-12157 — oak-segment-tar Guava-shim caches
+ OAK-12158 — oak-segment-tar CacheLIRS-based caches
+ OAK-12159 — oak-blob-plugins
+ OAK-12160 — oak-run-commons
+
+Sequential tail:
+ OAK-12161 — oak-it-osgi verification (depends on OAK-12149 through OAK-12160)
+ OAK-12162 — final cleanup (depends on OAK-12161)
+```
+
+## Migration rules (enforced for every task)
+
+**1. Guava-free check** — Before declaring a migration task done, run:
+```bash
+grep -rn "org.apache.jackrabbit.guava.common.cache" /src/
+```
+This must return **zero results** — both `src/main/java` and `src/test/java` must be clean.
+Test files that reference Guava cache types (e.g. for reflective access checks) must be
+migrated in the same PR as the production code.
+
+**2. Cross-module return-type cascade** — Whenever a task changes the return type of any
+public method (e.g. `getCacheStats()`, `getCurrentStats()`), every caller across **all
+modules** must be updated in the same PR. Before closing the task:
+```bash
+grep -rn "methodName()" $(git rev-parse --show-toplevel)
+```
+A caller in an unrelated module that still expects the old return type will compile
+locally (if that module is not rebuilt) but will fail in CI's full build. The list of
+known callers must be explicitly enumerated in the task's "What changes" section.
+
+---
+
+## TASK-1 — Oak Cache API interfaces [oak-core-spi] — [OAK-12147](https://issues.apache.org/jira/browse/OAK-12147)
+
+**Depends on:** none
+**Independent of:** none (all other tasks depend on this transitively)
+
+### What changes
+
+- `oak-core-spi/.../cache/api/Cache.java` — **new** interface:
+ - `getIfPresent(K)` → `@Nullable V`
+ - `get(K, Function super K, ? extends V>)` → `@Nullable V` (matches Caffeine's manual-cache contract)
+ - `put(K, V)`
+ - `invalidate(K)`
+ - `invalidateAll()`
+ - `invalidateAll(Iterable)` — _(no Oak module currently calls this; can be removed if decided)_
+ - `estimatedSize()` → `long` — _(no Oak module currently calls this directly; can be removed if decided)_
+ - `stats()` → `CacheStatsSnapshot`
+ - `asMap()` → `ConcurrentMap`
+ - `getAllPresent(Iterable)` → `Map` — _(no Oak module currently calls this; CacheLIRS throws `UnsupportedOperationException` for it; can be removed if decided)_
+ - `cleanUp()` — _(no Oak module currently calls this; CacheLIRS is a no-op; can be removed if decided)_
+
+- `oak-core-spi/.../cache/api/LoadingCache.java` — **new** interface extending `Cache`:
+ - `get(K)` → `@NotNull V` (runtime exceptions propagate directly; checked loader failures surface as `CompletionException`)
+ - `refresh(K)` → `CompletableFuture`
+
+- `oak-core-spi/.../cache/api/CacheLoader.java` — **new** functional interface (`V load(K) throws Exception`)
+- `oak-core-spi/.../cache/api/Weigher.java` — **new** functional interface (`int weigh(K, V)`)
+- `oak-core-spi/.../cache/api/EvictionCause.java` — **new** enum (`EXPLICIT`, `REPLACED`, `SIZE`, `EXPIRED`, `COLLECTED`)
+- `oak-core-spi/.../cache/api/EvictionListener.java` — **new** functional interface (`void onEviction(K, V, EvictionCause)`)
+- `oak-core-spi/.../cache/api/CacheStatsSnapshot.java` — **new** immutable value object: `hitCount`, `missCount`, `loadSuccessCount`, `loadFailureCount`, `totalLoadTime`, `evictionCount`; methods `minus()`, `hitRate()`, `missRate()`, `requestCount()`
+- `oak-core-spi/.../cache/AbstractCacheStats.java` — **no change** (still returns Guava shim `CacheStats` from `getCurrentStats()`; decoupled in OAK-12162)
+- `oak-core-spi/.../cache/CacheStats.java` — **no change** (existing Guava-wrapping JMX class; deferred to OAK-12162)
+
+### Acceptance criteria
+- `oak-core-spi` compiles; no new Caffeine or Guava types in the public API surface
+- All existing `oak-core-spi` tests pass (`CacheTest`, `CacheSizeTest`, `ConcurrentTest`, `ConcurrentPerformanceTest`)
+- No consumer module is changed; all existing `CacheLIRS.newBuilder()` and `new CacheStats(guavaCache, ...)` call sites still compile
+
+---
+
+## TASK-2 — Hidden implementations and builder [oak-core-spi] — [OAK-12148](https://issues.apache.org/jira/browse/OAK-12148)
+
+**Depends on:** OAK-12147
+**Independent of:** none (all consumer tasks depend on this)
+
+### What changes
+- `oak-core-spi/.../cache/api/CacheBuilder.java` — **new** public final class for creating Caffeine-backed Oak caches only.
+
+ Builder fields: `maximumWeight`, `maximumSize`, `weigher(Weigher)`, `evictionListener(EvictionListener)`, `recordStats`, `expireAfterAccess`, `expireAfterWrite`, `refreshAfterWrite`.
+ Methods: `build()` → `Cache`, `build(CacheLoader)` → `LoadingCache`.
+ `build()` must always return a manual-cache adapter that does not implement `LoadingCache`; `build(CacheLoader)` must always return a loading-cache adapter.
+ Validation rules are enforced in the builder before cache construction:
+ - exactly one of `maximumSize` or `maximumWeight` must be configured
+ - `maximumWeight` requires `weigher(...)`
+ - `weigher(...)` requires `maximumWeight(...)`
+ - `refreshAfterWrite(...)` is valid only with `build(CacheLoader)`
+ `CacheBuilder` contains no `CacheLIRS` references; callers that still need LIRS must build
+ loading `CacheLIRS` instances directly and expose them through `CacheLIRS.asOakCache()`.
+
+- `oak-core-spi/.../cache/CacheLIRS.java` — add `asOakCache()` returning an Oak `LoadingCache` view for `CacheLIRS` instances created with a loader
+
+- `oak-core-spi/.../cache/impl/lirs/LirsCacheAdapter.java` — **new** package-private class implementing `Cache`, wrapping `CacheLIRS`; paired with `LirsLoadingCacheAdapter` for loading caches
+ - Adapts `Weigher` → Guava shim `Weigher`
+ - Adapts `EvictionListener`/`EvictionCause` → `CacheLIRS.EvictionCallback`/Guava `RemovalCause`
+ - `get(K, Function)`: adapts the key-aware mapping function to `CacheLIRS.get(K, Callable)` and converts checked failures to `CompletionException`
+ - `LirsLoadingCacheAdapter.get(K)`: delegates to `CacheLIRS.get(K)` and converts checked failures to `CompletionException`
+ - `LirsLoadingCacheAdapter.refresh(K)`: runs CacheLIRS refresh and returns a completed future representing the best-effort synchronous refresh result
+ - `stats()`: converts Guava shim `CacheStats` → `CacheStatsSnapshot`
+ - `invalidateAll(Iterable)`, `estimatedSize()`, `getAllPresent()`, `cleanUp()`: delegate directly to CacheLIRS
+
+- `oak-core-spi/.../cache/impl/caffeine/CaffeineCacheAdapter.java` — **new** package-private class implementing `Cache`, wrapping Caffeine `Cache`; paired with `CaffeineLoadingCacheAdapter` for loading caches:
+ - `get(K, Function)`: delegates directly to Caffeine's manual-cache API
+ - `CaffeineLoadingCacheAdapter.get(K)`: delegates directly to Caffeine's loading-cache API
+ - `CaffeineLoadingCacheAdapter.refresh(K)`: delegates directly to Caffeine and returns the refresh future
+ - Adapts `Weigher` → Caffeine `Weigher`, `EvictionListener`/`EvictionCause` → Caffeine `RemovalListener`/`RemovalCause`
+ - `stats()`: converts Caffeine `CacheStats` → `CacheStatsSnapshot`
+
+- `oak-core-spi/.../cache/api/CacheStatsAdapter.java` — **new** package-private class; extends `AbstractCacheStats`; overrides `getCurrentStats()` returning Guava shim `CacheStats` converted from the wrapped `Cache`'s `CacheStatsSnapshot` — Guava return type kept until OAK-12162 changes the base class
+
+- `oak-core-spi/.../cache/EmpiricalWeigher.java` — modify to implement `Weigher` while keeping a temporary Guava-compatible bridge for existing callers until OAK-12162 cleanup
+
+- `oak-core-spi/pom.xml` — add Caffeine as `compile` scope dependency (used only inside `CaffeineCacheAdapter`; not re-exported)
+
+**Note on `put(K, V, int memory)` (CacheLIRS-specific):** `DefaultSegmentWriter` calls
+`nodeCache.put(key, value, memoryCost)`. This method has no equivalent in `Cache` or
+Caffeine — Caffeine derives weight from the configured `Weigher` at insertion time.
+Migration for this call site (OAK-12158): replace with `Cache.put(key, value)` and
+ensure an `Weigher` is set on the builder that computes the same cost from the
+key/value. `LirsCacheAdapter` does **not** expose `put(K, V, int memory)` on the interface.
+
+### Restore javadoc links from OAK-12147
+OAK-12147 deferred `{@link CacheBuilder}` references as plain `{@code}` text with `TODO OAK-TASK2` comments.
+Restore all of them to proper `{@link}` in this task:
+
+| File | Links to restore |
+|------|-----------------|
+| `Cache.java` | `{@link CacheBuilder}`, `{@link CacheBuilder#recordStats()}` |
+| `CacheLoader.java` | `{@link CacheBuilder#build(CacheLoader)}` |
+| `Weigher.java` | `{@link CacheBuilder#weigher(Weigher)}`, `{@link CacheBuilder#maximumWeight(long)}` |
+| `EvictionListener.java` | `{@link CacheBuilder#removalListener(EvictionListener)}` |
+| `LoadingCache.java` | `{@link CacheBuilder#build(CacheLoader)}` |
+
+Remove all `` HTML comments after restoring the links.
+
+### Acceptance criteria
+- All existing `oak-core-spi` tests pass
+- New focused unit tests cover:
+ - `CacheBuilderTest`
+ - `build()` creates a Caffeine-backed manual cache
+ - `build()` returns a manual `Cache` that does not implement `LoadingCache`
+ - `build(CacheLoader)` creates a Caffeine-backed loading cache
+ - `weigher` and `evictionListener` wiring
+ - `build(CacheLoader)` produces `LoadingCache`; checked loader failure surfaces as `CompletionException`
+ - runtime loader failures propagate directly
+ - `Cache.get(key, mappingFunction)` uses Caffeine's `Function` contract and propagates runtime failures directly
+ - `LoadingCache.refresh(key)` returns a non-null `CompletableFuture`
+ - invalid builder combinations are rejected consistently before cache construction
+ - `CacheStatsAdapter` bridges `CacheStatsSnapshot` back to Guava shim `CacheStats`
+ - `stats()` returns non-null `CacheStatsSnapshot` with correct counts
+ - `CaffeineCacheAdapterTest`
+ - Caffeine removal-cause mapping, stats snapshot conversion, and iterable invalidation
+ - `LirsCacheAdapterTest`
+ - LIRS removal-cause mapping, checked/runtime/error exception translation, and stats snapshot conversion
+ - `LirsLoadingCacheAdapterTest`
+ - loading LIRS get/refresh behavior and checked/runtime loader failure translation
+ - `CacheLIRSOakAdapterTest`
+ - `CacheLIRS.asOakCache()` succeeds only for loading caches and rejects manual caches
+- No `TODO OAK-TASK2` comments remain in any file
+- No consumer module is changed; existing call sites still compile
+
+### Compatibility note for downstream tasks
+For OAK-12149 through OAK-12160, the Oak cache API follows the Caffeine cache contract.
+Migration in those tasks must update manual cache loads from `Callable` to
+`Function`, and callers of `loadingCache.get(key)` must stop relying on checked
+`ExecutionException`.
+
+---
+
+## TASK-3 — Migrate oak-blob-cloud to Oak Cache API [oak-blob-cloud] — [OAK-12149](https://issues.apache.org/jira/browse/OAK-12149)
+
+**Depends on:** OAK-12148
+**Independent of:** OAK-12150, OAK-12151, OAK-12152, OAK-12153, OAK-12154, OAK-12155, OAK-12156, OAK-12157, OAK-12158, OAK-12159, OAK-12160
+
+### What changes
+- `oak-blob-cloud/.../s3/S3Backend.java` — replace `CacheBuilder.newBuilder()...build()` with `CacheBuilder.newBuilder()...build()`; replace `Cache` field with `Cache`; remove Guava shim cache imports
+
+### Exception handling migration
+- Callers of `cache.get(key, callable)` must switch to `cache.get(key, k -> ...)`
+- Callers of `loadingCache.get(key)` must stop catching checked `ExecutionException`; runtime failures propagate directly and checked loader failures surface as `CompletionException`
+
+### Acceptance criteria
+- No `org.apache.jackrabbit.guava.common.cache` imports in `oak-blob-cloud/src/` (main and test)
+- `S3Backend` cache tests pass (presigned URI caching, expiry, hit/miss)
+
+---
+
+## TASK-4 — Migrate oak-blob-cloud-azure to Oak Cache API [oak-blob-cloud-azure] — [OAK-12150](https://issues.apache.org/jira/browse/OAK-12150)
+
+**Depends on:** OAK-12148
+**Independent of:** OAK-12149, OAK-12151, OAK-12152, OAK-12153, OAK-12154, OAK-12155, OAK-12156, OAK-12157, OAK-12158, OAK-12159, OAK-12160
+
+### What changes
+- `oak-blob-cloud-azure/.../AzureBlobStoreBackend.java` — replace `CacheBuilder.newBuilder()` with `CacheBuilder`; replace `Cache` with `Cache`; remove Guava shim cache imports
+- `oak-blob-cloud-azure/.../v8/AzureBlobStoreBackendV8.java` — same changes
+
+### Exception handling migration
+- Callers of `cache.get(key, callable)` must switch to `cache.get(key, k -> ...)`
+- Callers of `loadingCache.get(key)` must stop catching checked `ExecutionException`; runtime failures propagate directly and checked loader failures surface as `CompletionException`
+
+### Acceptance criteria
+- No `org.apache.jackrabbit.guava.common.cache` imports in `oak-blob-cloud-azure/src/` (main and test)
+- Azure backend cache tests pass
+
+---
+
+## TASK-5 — Migrate oak-blob to Oak Cache API [oak-blob] — [OAK-12151](https://issues.apache.org/jira/browse/OAK-12151)
+
+**Depends on:** OAK-12148
+**Independent of:** OAK-12149, OAK-12150, OAK-12152, OAK-12153, OAK-12154, OAK-12155, OAK-12156, OAK-12157, OAK-12158, OAK-12159, OAK-12160
+
+### What changes
+- `oak-blob/.../split/BlobIdSet.java` — replace `CacheBuilder.newBuilder()` with `CacheBuilder`; replace `Cache` with `Cache`; remove Guava shim cache imports
+
+### Exception handling migration
+- Callers of `cache.get(key, callable)` must switch to `cache.get(key, k -> ...)`
+- Callers of `loadingCache.get(key)` must stop catching checked `ExecutionException`; runtime failures propagate directly and checked loader failures surface as `CompletionException`
+
+### Acceptance criteria
+- No `org.apache.jackrabbit.guava.common.cache` imports in `oak-blob/src/` (main and test)
+- `BlobIdSet` membership and bounded-cache tests pass
+
+---
+
+## TASK-6 — Migrate oak-search-elastic to Oak Cache API [oak-search-elastic] — [OAK-12152](https://issues.apache.org/jira/browse/OAK-12152)
+
+**Depends on:** OAK-12148
+**Independent of:** OAK-12149, OAK-12150, OAK-12151, OAK-12153, OAK-12154, OAK-12155, OAK-12156, OAK-12157, OAK-12158, OAK-12159, OAK-12160
+
+### What changes
+- `oak-search-elastic/.../ElasticIndexStatistics.java` — replace `CacheBuilder.newBuilder()` with `CacheBuilder`; replace `LoadingCache` fields with `LoadingCache`; replace `CacheLoader` with `CacheLoader` passed to `CacheBuilder.build(loader)`; remove Guava shim cache imports
+
+### Exception handling migration
+- Callers of `cache.get(key, callable)` must switch to `cache.get(key, k -> ...)`
+- Callers of `loadingCache.get(key)` must stop catching checked `ExecutionException`; runtime failures propagate directly and checked loader failures surface as `CompletionException`
+
+### CacheBuilder ticker API (added to `oak-core-spi`)
+- `ticker(Supplier ticker)` — raw nanosecond supplier
+- `ticker(Clock clock)` — convenience overload; delegates to the `Supplier` overload via `() -> TimeUnit.MILLISECONDS.toNanos(clock.millis())`
+
+### Acceptance criteria
+- No `org.apache.jackrabbit.guava.common.cache` imports in `oak-search-elastic/src/` (main and test)
+- `ElasticIndexStatisticsTest` passes covering expiry, refresh, and loader failure behavior
+- Both `ticker(Supplier)` and `ticker(Clock)` present on `CacheBuilder`
+
+---
+
+## TASK-7 — Migrate oak-search to Oak Cache API [oak-search] — [OAK-12153](https://issues.apache.org/jira/browse/OAK-12153)
+
+**Depends on:** OAK-12148
+**Independent of:** OAK-12149, OAK-12150, OAK-12151, OAK-12152, OAK-12154, OAK-12155, OAK-12156, OAK-12157, OAK-12158, OAK-12159, OAK-12160
+
+### What changes
+- `oak-search/.../ExtractedTextCache.java` — replace `CacheBuilder.newBuilder()` with `CacheBuilder`; replace `Cache` with `Cache`; replace local `EmpiricalWeigher` inner class (Guava `Weigher`) with `Weigher` lambda; replace `new CacheStats(guavaCache, ...)` with `CacheStatsAdapter`; remove all Guava shim cache imports
+- `oak-lucene/pom.xml` — add `org/apache/jackrabbit/oak/cache/api/CacheStatsAdapter.class` to the `Embed-Dependency` inline list for `oak-core-spi`; fixes OSGi classloader split where `oak-lucene`'s inlined `AbstractCacheStats` was a different class than `oak-core-spi` bundle's `AbstractCacheStats` (the superclass of `CacheStatsAdapter`), causing `VerifyError` in `IndexVersionSelectionIT`
+
+**Return-type cascade** — `ExtractedTextCache.getCacheStats()` changes from `CacheStats` to
+`AbstractCacheStats`. All callers across all modules must be updated in the same PR:
+- `oak-lucene/.../LuceneIndexProviderService.java` — `CacheStats` → `AbstractCacheStats`
+- `oak-run-commons/.../DocumentStoreIndexerBase.java` — `CacheStats` → `AbstractCacheStats`
+
+Run `grep -rn "getCacheStats()"` at the repo root before closing to confirm no caller is missed.
+
+### Exception handling migration
+- Callers of `cache.get(key, callable)` must switch to `cache.get(key, k -> ...)`
+- Callers of `loadingCache.get(key)` must stop catching checked `ExecutionException`; runtime failures propagate directly and checked loader failures surface as `CompletionException`
+
+### Acceptance criteria
+- No `org.apache.jackrabbit.guava.common.cache` imports in `oak-search/src/` (main and test)
+- `ExtractedTextCache` cache tests pass, including stats reporting
+- `DocumentStoreIndexerBase` compiles cleanly in `oak-run-commons`
+
+---
+
+## TASK-8 — Migrate oak-store-document cache infrastructure [oak-store-document] — [OAK-12154](https://issues.apache.org/jira/browse/OAK-12154)
+
+**Depends on:** OAK-12148
+**Independent of:** OAK-12149, OAK-12150, OAK-12151, OAK-12152, OAK-12153, OAK-12155, OAK-12156, OAK-12157, OAK-12158, OAK-12159, OAK-12160
+
+### What changes
+- `DocumentNodeStoreBuilder.java` — replace `CacheLIRS.newBuilder()` calls with `CacheBuilder` for Caffeine-backed caches
+- `cache/NodeDocumentCache.java` — `Cache` types to `Cache`; `asMap()` calls unchanged (method exists on `Cache`)
+- `cache/ForwardingListener.java` — `RemovalCause` to `EvictionCause`; update to use `EvictionListener`
+- `persistentCache/EvictionListener.java` — `RemovalCause` to `EvictionCause`
+- `CachingCommitValueResolver.java` — `Cache` to `Cache`
+- `CacheStats` construction sites — replace with stats obtained from `Cache` via `CacheStatsAdapter`
+- `BranchTest.java` — update `Cache` import from Guava shim to Oak API (`org.apache.jackrabbit.oak.cache.api.Cache`)
+
+### Exception handling migration
+- Callers of `cache.get(key, callable)` must switch to `cache.get(key, k -> ...)`
+- Callers of `loadingCache.get(key)` must stop catching checked `ExecutionException`; runtime failures propagate directly and checked loader failures surface as `CompletionException`
+
+### Acceptance criteria
+- `NodeDocumentCacheTest`, `CacheChangesTrackerTest`, `AsyncCacheTest`, `DisableCacheTest`, `BranchTest` pass
+- Checked loader failures surface as `CompletionException` on the Oak-visible API
+- Synchronous eviction callback timing preserved
+
+---
+
+## TASK-9 — Migrate oak-store-document diff caches [oak-store-document] — [OAK-12155](https://issues.apache.org/jira/browse/OAK-12155)
+
+**Depends on:** OAK-12148
+**Independent of:** OAK-12149, OAK-12150, OAK-12151, OAK-12152, OAK-12153, OAK-12154, OAK-12156, OAK-12157, OAK-12158, OAK-12159, OAK-12160
+
+### What changes
+- `MemoryDiffCache.java` — `Cache` to `Cache`; replace Guava/CacheLIRS builder with `CacheBuilder`
+- `LocalDiffCache.java` — `Cache` to `Cache`; replace builder
+- `TieredDiffCache.java` — `Cache` to `Cache`; update type references
+
+### Exception handling migration
+- Callers of `cache.get(key, callable)` must switch to `cache.get(key, k -> ...)`
+- Callers of `loadingCache.get(key)` must stop catching checked `ExecutionException`; runtime failures propagate directly and checked loader failures surface as `CompletionException`
+
+### Acceptance criteria
+- `MemoryDiffCacheTest`, `LocalDiffCacheTest` pass
+- Diff cache eviction behavior unchanged
+
+---
+
+## TASK-10 — Migrate oak-store-document persistent cache and stores [oak-store-document] — [OAK-12156](https://issues.apache.org/jira/browse/OAK-12156)
+
+**Depends on:** OAK-12148
+**Independent of:** OAK-12149, OAK-12150, OAK-12151, OAK-12152, OAK-12153, OAK-12154, OAK-12155, OAK-12157, OAK-12158, OAK-12159, OAK-12160
+
+### What changes
+- `persistentCache/NodeCache.java` — `Cache` to `Cache`
+- `persistentCache/PersistentCache.java` — `Cache` to `Cache`
+- `DocumentNodeStore.java` — change `Cache` types to `Cache`
+- `NodeDocument.java` — update cache type references
+- `MongoDocumentStore.java` — remove direct Caffeine/Guava cache references
+- `RDBDocumentStore.java` — remove direct Caffeine/Guava cache references
+- `MemoryDocumentStore.java` — update if it references cache types
+- `JournalDiffLoader.java` — update if it references cache types
+- Various test classes — update to use `Cache` types
+
+### Exception handling migration
+- Callers of `cache.get(key, callable)` must switch to `cache.get(key, k -> ...)`
+- Callers of `loadingCache.get(key)` must stop catching checked `ExecutionException`; runtime failures propagate directly and checked loader failures surface as `CompletionException`
+
+### Acceptance criteria
+- No `org.apache.jackrabbit.guava.common.cache` imports in `oak-store-document/src/` (main and test)
+- Persistent cache eviction behavior unchanged
+- `persistentCache.CacheTest`, `persistentCache.NodeCacheTest` pass
+
+---
+
+## TASK-11 — Migrate oak-segment-tar Guava-shim caches [oak-segment-tar] — [OAK-12157](https://issues.apache.org/jira/browse/OAK-12157)
+
+**Depends on:** OAK-12148
+**Independent of:** OAK-12149, OAK-12150, OAK-12151, OAK-12152, OAK-12153, OAK-12154, OAK-12155, OAK-12156, OAK-12158, OAK-12159, OAK-12160
+
+### What changes
+- `SegmentCache.java` — `CacheBuilder.newBuilder()` (Guava shim) to `CacheBuilder`; `Cache` to `Cache`; Guava `RemovalCause` to `EvictionCause`; inner `Stats` class updated to convert `CacheStatsSnapshot` → Guava shim `CacheStats` in `getCurrentStats()` (Guava return type kept until OAK-12162)
+- `RecordCache.java` — `CacheBuilder.newBuilder()` (Guava shim) to `CacheBuilder`; `Cache` to `Cache`; Guava `Weigher` to `Weigher`
+- `CacheWeights.java` — `Weigher` to `Weigher`
+- `CachingSegmentReader.java` — update cache type references
+
+### Exception handling migration
+- Callers of `cache.get(key, callable)` must switch to `cache.get(key, k -> ...)`
+- Callers of `loadingCache.get(key)` must stop catching checked `ExecutionException`; runtime failures propagate directly and checked loader failures surface as `CompletionException`
+
+### Acceptance criteria
+- `SegmentCacheTest` passes
+- Segment unload timing and eviction callback timing unchanged
+
+---
+
+## TASK-12 — Migrate oak-segment-tar CacheLIRS-based caches [oak-segment-tar] — [OAK-12158](https://issues.apache.org/jira/browse/OAK-12158)
+
+**Depends on:** OAK-12148
+**Independent of:** OAK-12149, OAK-12150, OAK-12151, OAK-12152, OAK-12153, OAK-12154, OAK-12155, OAK-12156, OAK-12157, OAK-12159, OAK-12160
+
+### What changes
+- `ReaderCache.java` — `CacheLIRS.newBuilder()` to `CacheBuilder`; `CacheLIRS` to `Cache`
+- `WriterCacheManager.java` — update cache type references
+- `PriorityCache.java` — update if it references Guava shim cache types directly
+- `RecordCacheStats.java` — update to obtain stats from the migrated `Cache`; convert `CacheStatsSnapshot` → Guava shim `CacheStats` in `getCurrentStats()` (Guava return type kept until OAK-12162)
+- `spi/persistence/persistentcache/SegmentCacheStats.java` — same: convert `CacheStatsSnapshot` → Guava shim `CacheStats` in `getCurrentStats()` until OAK-12162
+- `SegmentNodeStoreRegistrar.java` — update if it references cache builder types
+- **`DefaultSegmentWriter.java`** — `nodeCache.put(key, value, memoryCost)` must be replaced with `nodeCache.put(key, value)`. Add an `Weigher` to the `CacheBuilder` configuration that computes the same memory cost from the key/value, so Caffeine can use it at insertion time. The CacheLIRS-specific 3-arg `put` is not on `Cache`.
+
+### Exception handling migration
+- Callers of `cache.get(key, callable)` must switch to `cache.get(key, k -> ...)`
+- Callers of `loadingCache.get(key)` must stop catching checked `ExecutionException`; runtime failures propagate directly and checked loader failures surface as `CompletionException`
+
+### Acceptance criteria
+- No `org.apache.jackrabbit.guava.common.cache` imports in `oak-segment-tar/src/` (main and test)
+- `PriorityCacheTest`, `ConcurrentPriorityCacheTest`, `ReaderCacheTest` pass
+- Memoization behavior unchanged
+
+---
+
+## TASK-13 — Migrate oak-blob-plugins to Oak Cache API [oak-blob-plugins] — [OAK-12159](https://issues.apache.org/jira/browse/OAK-12159)
+
+**Depends on:** OAK-12148
+**Independent of:** OAK-12149, OAK-12150, OAK-12151, OAK-12152, OAK-12153, OAK-12154, OAK-12155, OAK-12156, OAK-12157, OAK-12158, OAK-12160
+
+### What changes
+- `FileCache.java` — `CacheLIRS.newBuilder()` to `CacheBuilder`; `Cache` to `Cache`; `CacheLIRS.EvictionCallback` to `EvictionListener`; `CacheStatsSnapshot` to stats from `CacheBuilder`
+- `UploadStagingCache.java` — update cache types
+- `CompositeDataStoreCache.java` — update cache types
+- `AbstractSharedCachingDataStore.java` — update cache types
+- `CachingBlobStore.java` — update cache types
+- `DataStoreCacheUpgradeUtils.java` — update cache types
+
+### Exception handling migration
+- Callers of `cache.get(key, callable)` must switch to `cache.get(key, k -> ...)`
+- Callers of `loadingCache.get(key)` must stop catching checked `ExecutionException`; runtime failures propagate directly and checked loader failures surface as `CompletionException`
+
+### Acceptance criteria
+- No `org.apache.jackrabbit.guava.common.cache` imports in `oak-blob-plugins/src/` (main and test)
+- No `CacheLIRS` references in `oak-blob-plugins/src/` (main and test)
+- File cache eviction/deletion behavior unchanged
+- All blob-plugins cache tests pass
+
+---
+
+## TASK-14 — Migrate oak-run-commons and remaining modules [oak-run-commons] — [OAK-12160](https://issues.apache.org/jira/browse/OAK-12160)
+
+**Depends on:** OAK-12148
+**Independent of:** OAK-12149, OAK-12150, OAK-12151, OAK-12152, OAK-12153, OAK-12154, OAK-12155, OAK-12156, OAK-12157, OAK-12158, OAK-12159
+
+### What changes
+- `oak-run-commons/.../DocumentNodeStoreHelper.java` — `CacheLIRS` to `Cache`
+- `oak-run-commons/.../DocumentStoreIndexerBase.java` — update if it references cache types
+- Scan for any other modules with residual Caffeine/Guava cache imports and migrate them
+
+### Exception handling migration
+- Callers of `cache.get(key, callable)` must switch to `cache.get(key, k -> ...)`
+- Callers of `loadingCache.get(key)` must stop catching checked `ExecutionException`; runtime failures propagate directly and checked loader failures surface as `CompletionException`
+
+### Acceptance criteria
+- No direct Caffeine or Guava cache usage in any module's `src/` (main or test) except `oak-core-spi`
+- `DocumentNodeStoreHelper` tests pass
+
+---
+
+## TASK-15 — OSGi integration verification [oak-it-osgi] — [OAK-12161](https://issues.apache.org/jira/browse/OAK-12161)
+
+**Depends on:** OAK-12149, OAK-12150, OAK-12151, OAK-12152, OAK-12153, OAK-12154, OAK-12155, OAK-12156, OAK-12157, OAK-12158, OAK-12159, OAK-12160
+**Independent of:** none
+
+### What changes
+- Verify all OSGi bundle manifests: consumer bundles must not import `com.github.benmanes.caffeine.cache` or `org.apache.jackrabbit.guava.common.cache`
+- Remove Caffeine from `Import-Package` of consumer bundles if still present
+- Verify `oak-core-spi` is the only bundle importing Caffeine packages
+- Run OSGi integration tests (`oak-it-osgi`)
+
+**Known limitation (OAK-3598):** `oak-lucene` selectively inlines `AbstractCacheStats.class`, `CacheStats.class`, and (after OAK-12153) `CacheStatsAdapter.class` from `oak-core-spi`. The rest of `org.apache.jackrabbit.oak.cache.api` (`Cache`, `Weigher`, `CacheBuilder`) is not auto-added to `Import-Package` by bnd due to the split-package heuristic. This means `CacheStatsAdapter` methods that resolve `Cache`/`Weigher` at runtime will fail in OSGi if `maxWeight > 0`. The `IndexVersionSelectionIT` test is safe because `LuceneIndexEditorProvider` defaults to `maxWeight = 0`. The full fix requires OAK-3598 (remove selective inlining, add proper `Import-Package`).
+
+### Acceptance criteria
+- `mvn verify -pl oak-it-osgi -PintegrationTesting` passes
+- No consumer bundle imports Caffeine packages
+
+---
+
+## TASK-16 — Final cleanup and deprecation [oak-core-spi] — [OAK-12162](https://issues.apache.org/jira/browse/OAK-12162)
+
+**Depends on:** OAK-12161
+**Independent of:** none
+
+> **This task is a cleanup gate.** `CacheBuilder` already produces only Caffeine-backed
+> caches. What remains is removing the transitional LIRS adapter code and the `CacheLIRS`
+> class itself once every consumer has migrated away from `CacheLIRS.asOakCache()`.
+> Execute this task only after OAK-12149 through OAK-12160 are all merged and no module
+> outside `oak-core-spi` references `CacheLIRS` or its adapters.
+
+### What changes
+- `AbstractCacheStats.java` — change `getCurrentStats()` return type from Guava shim `CacheStats` to `CacheStatsSnapshot`; rewrite internal `lastSnapshot` field and `stats()` method to use `CacheStatsSnapshot` arithmetic
+- `CacheStats.java` — update `getCurrentStats()` to return `CacheStatsSnapshot` (converting from its held Guava snapshot)
+- `CacheStatsAdapter.java` — update `getCurrentStats()` to return `CacheStatsSnapshot` directly (drop Guava conversion shim)
+- `GuavaCompatibleEmpiricalWeigher` — remove the temporary Guava-compatibility bridge introduced during the migration
+- `RecordCacheStats.java`, `SegmentCache.Stats`, `SegmentCacheStats.java` — update `getCurrentStats()` to return `CacheStatsSnapshot` directly (drop Guava conversion shims added in OAK-12157/OAK-12158)
+- Mark old `CacheStats` constructor (`Cache,?>` Guava shim) as `@Deprecated(forRemoval = true)`
+- **Remove `LirsCacheAdapter` and `LirsLoadingCacheAdapter`** — no longer needed once no consumer calls `CacheLIRS.asOakCache()`
+- **Remove `CacheLIRS`** (or mark `@Deprecated(forRemoval = true)` — since it was already `@Internal`, outright removal is acceptable)
+- Verify no module outside `oak-core-spi` references `CacheLIRS` or its `asOakCache()` bridge
+- Grep: confirm no module outside `oak-core-spi` imports `com.github.benmanes.caffeine.cache` or `org.apache.jackrabbit.guava.common.cache`
+- Remove any Guava cache shim re-exports if they still exist
+
+**Return-type cascade** — `AbstractCacheStats.getCurrentStats()` changes from Guava shim
+`CacheStats` to `CacheStatsSnapshot`. Every override and every direct call site across all
+modules must be updated in the same PR. Before starting, enumerate all callers:
+```bash
+grep -rn "getCurrentStats()" $(git rev-parse --show-toplevel)
+```
+Known overrides that must be updated (accumulated across OAK-12149 through OAK-12162):
+- `oak-core-spi/.../CacheStats.java` — override
+- `oak-core-spi/.../CacheStatsAdapter.java` — override
+- `oak-segment-tar/.../RecordCacheStats.java` — override (shim added in OAK-12158)
+- `oak-segment-tar/.../SegmentCache.Stats` — override (shim added in OAK-12157)
+- `oak-segment-tar/.../SegmentCacheStats.java` — override (shim added in OAK-12158)
+- Any other `AbstractCacheStats` subclass introduced in OAK-12149 through OAK-12160
+
+All callers of `getCurrentStats()` that store the result as `CacheStats` must also be updated
+to `CacheStatsSnapshot`.
+
+### Acceptance criteria
+- `mvn clean install -DskipTests` succeeds
+- `mvn clean install` succeeds (full test suite)
+- `AbstractCacheStats.getCurrentStats()` returns `CacheStatsSnapshot`; no Guava types in `AbstractCacheStats` or its subclasses
+- No Caffeine or Guava cache types in any public API surface outside `oak-core-spi`
+- No `CacheLIRS` or `LirsCacheAdapter` classes remain (or `CacheLIRS` is `@Deprecated(forRemoval = true)`)
+- `CacheBuilder` has no `lirs` code path — Caffeine is the sole implementation
+- OSGi integration tests pass