[← versus Hibernate L2](09-versus-hibernate-l2.md) · [next: diagnostics →](11-diagnostics.md) # 10. Caching and transactions ## The ordering The caching interceptor runs **inside** the transaction interceptor. A cache write therefore happens at method exit — before the commit, and without any knowledge of whether the commit will succeed. `docs/output/13-transactions.txt` puts an ordinary write-through update inside an outer transaction that then fails: ``` nameOf(1) -> Alice An outer @Transactional method calls the @CachePut update, which succeeds, and then fails on the next step. The transaction rolls back. what the cache serves : Alice Cooper what the database has : Alice ``` The cache is now holding a name no transaction ever committed. Nothing will correct it until the entry expires or something evicts it — and on the default provider nothing expires. The same shape with `@CacheEvict` is self-healing: ``` after the rollback, nameOf(2) -> Bob database loads: 2 <- the entry was evicted, so this one reloaded ``` That asymmetry is the practical takeaway. **An eviction that fires too early costs a lookup; a put that fires too early costs correctness.** When in doubt, evict. ## `TransactionAwareCacheManagerProxy` Wrapping the manager defers every put and evict to a post-commit synchronisation: ```java @Bean CacheManager cacheManager() { return new TransactionAwareCacheManagerProxy(new ConcurrentMapCacheManager()); } ``` `docs/output/19-transaction-aware.txt` runs the identical rollback and the cache still says `Alice`. Several `CacheManager` implementations expose the same thing as `setTransactionAware(true)` (they extend `AbstractTransactionSupportingCacheManager`); `TransactionAwareCacheManagerProxy` is the generic wrapper, and it lives in **`spring-context-support`**, not `spring-context`. ### What it does not cover - **Reads are never deferred.** A `@Cacheable` lookup inside the transaction sees whatever the shared cache holds. - **Outside a transaction it is a pass-through**, so the same method reached from an unmanaged path writes immediately. - **It does not make the cache transactional.** Two concurrent transactions still race at commit time; last writer wins, and it may be the one whose value is older. ### A correction worth recording `spring-framework#23192` reported that `beforeInvocation = true` was swallowed by the transaction-aware decorator — the evict got deferred to commit, which is the opposite of what the attribute asks for. That is widely repeated and, on 7.0.9, **no longer true**. The measurement in `docs/output/19-transaction-aware.txt` came back the other way round, and `docs/output/22-decorator-bytecode.txt` shows why: ``` doEvict(cache, key, true) -> Cache.evictIfPresent -> straight to the target cache doEvict(cache, key, false) -> Cache.evict -> registerSynchronization, runs after commit ``` `AbstractCacheInvoker.doEvict(Cache, Object, boolean)` picks `evictIfPresent` for the immediate path, and `TransactionAwareCacheDecorator` only registers a synchronisation in `evict`. `evictIfPresent` delegates straight to the target cache. This was written the wrong way round first and the run corrected it, which is a reasonable advertisement for running the thing. ## The pattern that actually holds up For anything that must not serve uncommitted state: 1. Evict, do not put — `@CacheEvict(beforeInvocation = true)`. 2. Make the cache transaction-aware if you also need the eviction not to happen on rollback. 3. Give every mutable cache a TTL, so the worst case is bounded even when all of the above is wrong. Related reading: [`@Transactional`: propagation, isolation and the silent failures](https://ankurm.com/transactional-propagation-isolation-silent-failures/) covers the interceptor ordering from the transaction side.