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

3.8 KiB

← providers and TTL · next: transactions →

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, the second-level cache and the three ways it stales your data, and configuring Ehcache 3 under it.

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.