Files
spring-boot-demo/caching/docs/07-sync-and-async.md
T
asmhatre a9867c0423 Add caching: the Spring cache abstraction, keys, eviction timing and the self-invocation trap
A Spring Boot 4.1.1 module whose test suite is the evidence for the article: 20 tests
producing 22 transcripts under docs/output/, plus 12 documentation chapters.

Findings the build pins:
- @EnableCaching has no exposeProxy attribute; the widely-copied
  @EnableCaching(exposeProxy = true) does not compile.
- Two methods sharing a cache name and an argument type share a key space, and one
  silently serves the other's answers.
- The documented cache-provider detection order does not match CacheType's enum
  order in 4.1.1: COUCHBASE before INFINISPAN, and CACHE2K before CAFFEINE.
- beforeInvocation = true is NOT deferred by TransactionAwareCacheManagerProxy on
  7.0.9 - doEvict picks evictIfPresent, which the decorator does not intercept.
- Four of five invalid declarations start a clean context and throw at the first call.
- Caffeine on the classpath silently displaces the simple provider.
2026-09-12 05:35:13 +00:00

3.0 KiB

← conditions and nulls · next: providers and TTL →

7. sync = true, and the async return types

The stampede

docs/output/11-stampede.txt, sixteen threads hitting one cold key, method sleeping 300 ms:

@Cacheable("reports")                     -> 16 invocations
@Cacheable("syncedReports", sync = true)  -> 1 invocation

Nothing is wrong with the unsynchronised version — it is doing exactly what it was told. Every thread that arrives during the 300 ms window finds a miss and runs the method. The problem is when this happens: right after a deployment, right after an eviction, right when the cache would have been most valuable. A cache that collapses under the load it was added to survive is a well-known way to turn a slow endpoint into an outage.

sync = true makes one caller compute while the others block on the same computation. It is implemented on top of Cache.get(key, Callable), so the provider has to support it; all the CacheManager implementations in the framework do.

What sync = true will not tolerate

Four restrictions, all enforced at the first call rather than at startup. Real messages from docs/output/20-invalid-declarations.txt:

Declaration Message
unless alongside sync A sync=true operation does not support the unless attribute on ...
two cache names A sync=true operation is restricted to a single cache on ...
combined with another cache operation A sync=true operation cannot be combined with other cache operations on ...

All three are IllegalStateException, thrown from the interceptor, on a context that started cleanly. An integration test that calls the method once is the cheapest possible insurance.

CompletableFuture and reactive types

Since Spring Framework 6.1 the cache annotations understand CompletableFuture, Mono and Flux. The interceptor unwraps the container and caches the emitted value.

The cache has to support future-based retrieval. ConcurrentMapCacheManager adapts on its own. CaffeineCacheManager does not, unless you say so — and the way it tells you is docs/output/17-async-cache-mode-missing.txt:

cacheManager : org.springframework.cache.caffeine.CaffeineCacheManager
The application started cleanly. Nothing warned about anything.

buildAsync("q3") ->
  java.lang.IllegalStateException: No Caffeine AsyncCache available: set CaffeineCacheManager.setAsyncCacheMode(true)

The fix is one line on the manager:

CaffeineCacheManager manager = new CaffeineCacheManager();
manager.setAsyncCacheMode(true);

With it on, docs/output/12-async-return-types.txt shows two calls and one supplier invocation.

Be careful how far you take this. The reference documentation's own caveat is worth quoting: annotation-driven caching "is not appropriate for sophisticated reactive interactions involving composition and back pressure" — a @Cacheable Flux stores a pre-collected list, which is rarely what a streaming endpoint wanted.