Files
spring-boot-demo/caching/docs/09-versus-hibernate-l2.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

69 lines
3.8 KiB
Markdown

[← providers and TTL](08-providers-and-ttl.md) · [next: transactions →](10-transactions.md)
# 9. This is not the Hibernate second-level cache
They are both called caching, they both make things faster, and they solve different problems at
different layers. Mixing them up produces designs that cache the wrong thing.
## Three layers, three scopes
| | Persistence context (L1) | Hibernate second-level cache (L2) | Spring cache abstraction |
|---|---|---|---|
| Scope | one `EntityManager` / transaction | one `SessionFactory`, all sessions | one `CacheManager`, whatever you annotate |
| Unit | managed entity instance | dehydrated entity state, by id | whatever object a method returned |
| Lookup by | identity map | entity id (plus query and collection regions) | a key the `KeyGenerator` built |
| Turned on by | always on | `hibernate.cache.use_second_level_cache` + `@Cache` | `@EnableCaching` + `@Cacheable` |
| Invalidated by | end of transaction | Hibernate, on write, automatically | you, with `@CacheEvict` |
| Knows about your database | yes | yes | **no** |
Ankur's Hibernate articles cover the first two in detail:
[the first-level cache](https://ankurm.com/master-hibernate-7-first-level-cache-the-secret-to-high-performance-java-apps/),
[the second-level cache and the three ways it stales your data](https://ankurm.com/mastering-hibernate-7-second-level-cache-boosting-performance-for-modern-java-apps/),
and [configuring Ehcache 3 under it](https://ankurm.com/master-hibernate-7-ehcache-3-configuration-high-performance-caching-with-jakarta-persistence/).
## The difference that bites
Hibernate's L2 stores *dehydrated state* — a flat array of column values keyed by entity id. On a
hit it rehydrates that state into a managed entity attached to the current session. Lazy
associations still work, because the resulting entity is live.
The Spring cache abstraction stores *the object your method returned*, as-is. If that object is a
JPA entity, what you cached is a detached entity with whatever its proxies were holding at the
moment the transaction closed. `docs/output/14-cached-entity.txt`:
```
loadEntity(1) -> Alice (com.ankurm.caching.jpa.Customer)
second call returns the same instance? true
--- touching the lazy collection outside the session ---
org.hibernate.LazyInitializationException
Cannot lazily initialize collection of role 'com.ankurm.caching.jpa.Customer.orders' with key '1' (no session)
```
Worse, it is the *same instance* every time. Every caller shares one mutable entity. One of them
calls a setter, and every other caller sees it — and if someone merges it back, they merge a
version number that may be stale.
## The rule
**Cache DTOs, not entities.** Map to a record inside the transaction and cache that. It is
immutable, it serialises cleanly to a distributed cache, it has no session affinity, and it makes
the cached shape an explicit decision rather than an accident of your mapping.
If you genuinely want entity caching — read-mostly reference data, keyed by id, invalidated
automatically on write — that is precisely what Hibernate's L2 is for, and it will do it better
than `@Cacheable` because it understands the writes.
## Which one to reach for
| You want to avoid | Use |
|---|---|
| re-loading the same entity by id across requests | Hibernate L2 (`@Cache` on the entity) |
| re-running the same query that returns entities | Hibernate query cache, carefully — it needs L2 for the entities too |
| re-running an expensive computation, HTTP call or aggregation | the Spring cache abstraction |
| re-building a response DTO from several sources | the Spring cache abstraction |
The two compose. A service method cached with `@Cacheable` that internally loads entities served
from L2 is a perfectly reasonable stack — just be clear about which layer is answering, because
only one of them will notice when the row changes.