Files
spring-boot-demo/caching/docs/05-eviction.md
T
asmhatre 66208bcd97 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: 19 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.
- 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:19:22 +00:00

76 lines
3.1 KiB
Markdown

[← keys](04-keys.md) · [next: conditions and nulls →](06-conditions-and-nulls.md)
# 5. Eviction, and when it happens
`@CacheEvict` defaults to `beforeInvocation = false`, which means: evict **after** the method
returns **normally**. A method that throws does not evict.
From `docs/output/08-evict-timing.txt`:
```
price("sku-1") -> 100 (stored price is 100)
updatePrice("sku-1", 250, fail=true) threw after writing the new price.
stored price now : 250
price("sku-1") : 100 <- the cache still serves the old value
```
The write to the real store happened. The exception came afterwards. The eviction did not run,
so the cache is now authoritative for a value that no longer exists anywhere else. Nothing will
correct it: the next reader gets 100, and the one after that, until something evicts the entry or
the process restarts.
`beforeInvocation = true` makes the same failure harmless:
```
updatePriceEvictFirst("sku-2", 250, fail=true) threw the same way.
price("sku-2") : 250 <- the entry went first, so the next read is honest
```
## Choosing between the three shapes
| Shape | Cost | Correct when the write fails? |
|---|---|---|
| `@CacheEvict` (default) | one miss on the next read | **no** — stale entry survives |
| `@CacheEvict(beforeInvocation = true)` | one miss, plus a window where two callers can both miss | yes |
| `@CachePut` | no miss at all | no — and it writes a value the store may not have |
`@CachePut` is the fastest and the most dangerous: it puts *your* computed value into the cache
without reading the store back, so any transformation the database applies — a trigger, a
default, a truncation, a generated column — is invisible to every subsequent reader. Use it when
the method's return value is definitively the new state.
`beforeInvocation = true` is the right default for anything that mutates. The extra miss costs
one lookup; the alternative costs an incident.
## `allEntries = true`
Clears the region in a single operation rather than key by key, which matters when the region is
large or remote. Two things to know:
- It is a blunt instrument in a shared cache: one bulk import evicts everything every other
caller warmed.
- On a distributed provider `clear()` may be an O(n) scan or a whole-keyspace operation. Check
what your provider does before putting it on a frequently called method.
## Errors from the cache itself
If the cache provider throws — a Redis timeout, a serialization failure — the default
`SimpleCacheErrorHandler` rethrows, so a cache outage becomes an application outage. A
`CacheErrorHandler` registered through `CachingConfigurer` can log and continue instead:
```java
@Configuration
@EnableCaching
class CacheConfig implements CachingConfigurer {
@Override
public CacheErrorHandler errorHandler() {
return new SimpleCacheErrorHandler() { /* log and swallow get/put/evict errors */ };
}
}
```
That is the right call for a read-through cache in front of a database, and the wrong call when
the eviction is what keeps two systems consistent — a swallowed evict error is a permanently
stale entry. Decide per cache, not per application.