Files
spring-boot-demo/caching/docs/06-conditions-and-nulls.md
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

2.3 KiB

← eviction · next: sync and async →

6. condition, unless, and what a cached null is

The two vetoes

condition is evaluated on the arguments before the method runs. A false condition skips the lookup and the write — the method behaves as though it were not annotated.

unless is evaluated after, can see #result, and vetoes the write only. The lookup still happened, so a cached value is still returned on a hit.

@Cacheable(cacheNames = "terms", condition = "#term.length() <= 8")
public String search(String term) { ... }

docs/output/09-conditions.txt: six characters, two calls, one invocation. Twenty-five characters, two calls, two invocations.

The useful pattern is exactly that one — refuse to cache inputs that will never repeat. A search box keyed on free text has a hit rate close to zero and will happily fill the heap.

null

By default a null return is cached. It is stored as a sentinel, org.springframework.cache.support.NullValue.INSTANCE, so that a hit on null is distinguishable from a miss. docs/output/10-nulls.txt shows it in the map:

  cache "nulls":
    key xyz        ->  null   [org.springframework.cache.support.NullValue]

This is usually what you want. Caching "not found" is the cheap defence against a hot lookup for a row that does not exist — the classic cache-penetration attack is a flood of requests for ids that are not in the database, and a cache that refuses to store misses passes every one of them straight through.

Turn it off when a null means "not loaded yet" rather than "not there":

@Cacheable(cacheNames = "terms", unless = "#result == null")

Or at the manager: ConcurrentMapCacheManager.setAllowNullValues(false), reachable through a CacheManagerCustomizer. Redis has its own switch, spring.cache.redis.cache-null-values, default true.

Note the asymmetry that catches people: unless = "#result == null" still performs the lookup, so if a null got into the cache some other way it will still be served. condition cannot help here — it cannot see the result.

For an Optional-returning method, #result is the unwrapped value, so the safe-navigation form is what you want: unless = "#result?.hardback".