[← 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.