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 javax.persistencejakarta.persistence namespace change, the ORM 6 query-engine rewrite, the Hibernate Search 6+ Elasticsearch backend), so the official documentation above wins on any discrepancy.

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 (javax.cache) + Caffeine

The lightweight, in-process default choice for a single-instance deployment — Caffeine as the JCache implementation behind Hibernate’s org.hibernate.cache.jcache.JCacheRegionFactory.

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

READ_ONLY

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 READ_ONLY-cached entity is a programming error.

NONSTRICT_READ_WRITE

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.

READ_WRITE

Uses soft locks around updates to prevent readers from ever seeing a stale value across a commit, at the cost of more bookkeeping than NONSTRICT_READ_WRITE. The right default for entities that are both cached and updated with any regularity.

TRANSACTIONAL

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

flowchart TB subgraph EMF["EntityManagerFactory (application-scoped)"] L2["Second-level cache\n(shared across EntityManagers, optional, provider-backed)"] end subgraph EM1["EntityManager 1"] L1a["First-level cache\n(this persistence context only)"] end subgraph EM2["EntityManager 2"] L1b["First-level cache\n(this persistence context only)"] end L1a --> L2 L1b --> L2 L2 --> DB[("Database")] L1a -.miss.-> DB

For Spring’s own @Cacheable abstraction and how it differs in scope from this entity/collection-level cache, see Caching.