Second-Level Cache
|
This section documents Hibernate ORM 7.4.x (User Guide, Introduction, Query Language Guide, Data Repositories Guide), Jakarta Persistence 3.2, Hibernate Search 8.4.x, and the Hibernate Validator / Hibernate Reactive references — which are the references these pages are written and verified against. This content was generated with the assistance of AI and should be verified against those official docs before being relied on in production. Three older reference books were consulted as bibliography only while preparing these pages and are not the
primary or main source for any page. All three predate Jakarta Persistence 3.2 and Hibernate ORM 6/7 (the
This section’s bibliography lists the reference material consulted while preparing these pages. |
Persistence Context & Lifecycle covers the always-on first-level cache. This page covers the optional, shared second-level cache — what it holds, the providers, and the concurrency strategies that decide what happens under concurrent access.
First-level cache recap
The first-level cache is the persistence context itself: mandatory, in-memory only, scoped to one
EntityManager, gone the moment that EntityManager closes. The second-level cache is everything the
first-level cache is not: optional, potentially off-heap/distributed, shared across every EntityManager
created from one EntityManagerFactory, and outlives any single unit of work.
Second-level cache architecture
Hibernate does not implement caching itself — it defines an SPI (org.hibernate.cache.spi.RegionFactory) and
delegates to a pluggable provider:
| Provider | Notes |
|---|---|
JCache ( |
The lightweight, in-process default choice for a single-instance deployment — Caffeine as the JCache
implementation behind Hibernate’s |
Infinispan |
A distributed, embeddable or client-server cache — the JBoss/Red Hat ecosystem’s own second-level cache provider, supports clustering out of the box. |
Ehcache |
Another long-standing JCache-compatible provider, with optional clustering (Terracotta). |
Redis |
Not a native Hibernate second-level cache provider by default, but reachable via a JCache-compliant Redis client or a community region-factory adapter — worth choosing when the application already runs Redis for other caching (see Caching) and wants one shared cache layer instead of two. |
A multi-instance deployment sharing an in-process (Caffeine) second-level cache is a correctness trap: each instance’s cache can silently diverge from what the others just wrote, since there is no cross-instance invalidation. Prefer a genuinely distributed provider (Infinispan, a shared Redis, Ehcache with clustering) once more than one application instance is in play.
@Cache and CacheConcurrencyStrategy
An entity opts in explicitly — nothing is cached by default:
@Entity
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE, region = "book")
public class Book {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// ...
}
| Strategy | Behavior |
|---|---|
|
Assumes the entity is never updated after insert — the simplest, fastest, and safest strategy, since there
is no update-time invalidation race to reason about. Attempting to update a |
|
Updates evict (rather than update) the cache entry, and there is a small window where a concurrent reader can see stale cached data right after an update commits, before eviction is visible everywhere. Acceptable when occasional staleness for a brief window is tolerable. |
|
Uses soft locks around updates to prevent readers from ever seeing a stale value across a commit, at the cost
of more bookkeeping than |
|
Fully transactional cache semantics (cache changes commit/rollback with the surrounding JTA transaction) — needs a cache provider that supports it (Infinispan does); the strongest guarantee, also the most demanding on the provider. |
@Cache also applies to a @OneToMany/@ManyToMany collection (caching the association’s identifiers, not
the associated entities' own state, which is cached separately per its own @Cache if present), and to
@NaturalId lookups via @NaturalIdCache.
The query cache and its pitfalls
The query cache caches the result set of identifiers a specific query+parameters combination returned, not
the entity data itself (which still comes from the entity/collection second-level cache, or the database if not
cached there). It requires hibernate.cache.use_query_cache=true and an explicit
query.setCacheable(true)/@QueryHints(@QueryHint(name = "org.hibernate.cacheable", value = "true")) per
query — nothing is query-cached implicitly. The known pitfall: every query cache region is invalidated
whenever any row of any table the query touches changes, regardless of whether that specific change would
have affected this specific query’s result — a query cache region for a frequently-updated table can end up
with a near-zero effective hit rate despite being "enabled," making it a net loss (cache-maintenance overhead
without a corresponding hit-rate benefit). Reserve the query cache for queries against tables that change
rarely.
CacheMode and SharedCacheMode
CacheMode (per-Session, or per-operation) controls how that session’s operations interact with the
second-level cache: NORMAL (read and write, the default), GET (read but never write), PUT (write but
never read — useful for a bulk-loading pass that should populate the cache without being slowed down by
reading it first), IGNORE (bypass entirely). SharedCacheMode (JPA, set at the persistence-unit level) sets
the default caching policy for entities that carry no explicit annotation:
ENABLE_SELECTIVE (only @Cacheable-annotated entities are cached — the default and generally the clearest),
DISABLE_SELECTIVE (cache everything except entities explicitly marked @Cacheable(false)), ALL, or NONE.
Cache regions and statistics
Every cached entity/collection/query lives in a named region (defaulting to the entity’s fully-qualified
class name, overridable via @Cache(region = "…")), which is also the unit of eviction
(SessionFactory.getCache().evictEntityData(Book.class), evictRegion("book")). hibernate.generate_statistics
plus SessionFactory.getStatistics() exposes hit/miss/put counts per region — the direct way to check whether
a given cache region is actually earning its keep before assuming it is, per the query-cache pitfall above; see
also Performance & Statistics.
First-level vs. second-level caching
For Spring’s own @Cacheable abstraction and how it differs in scope from this entity/collection-level cache,
see Caching.