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