Second-Level Cache In Depth

Configuring a second-level cache provider, cache regions and concurrency strategies, and when caching helps vs hurts.

What the second-level cache actually caches

The first-level cache (the persistence context) is mandatory and scoped to one session — covered on the Sessions & Caching page. The second-level cache is optional, configured explicitly, and shared across every session in the application, letting one request benefit from data a completely different request already loaded:

Plaintext
Session A: loads Product #42  --> hits the database, populates the 2nd-level cache
Session B: loads Product #42  --> finds it in the 2nd-level cache, NO database hit at all

It's a cache of entity state keyed by ID (and, separately, of query results if explicitly enabled) — not a cache of live Java object references, since those are still recreated per session from the cached state.

Configuring a cache provider

Hibernate doesn't implement caching storage itself — it delegates to a JCache (JSR-107) provider. Ehcache is the most common choice in the Spring ecosystem:

HTML
<dependency>
    <groupId>org.hibernate.orm</groupId>
    <artifactId>hibernate-jcache</artifactId>
</dependency>
<dependency>
    <groupId>org.ehcache</groupId>
    <artifactId>ehcache</artifactId>
</dependency>
Properties
spring.jpa.properties.hibernate.cache.use_second_level_cache=true
spring.jpa.properties.hibernate.cache.region.factory_class=org.hibernate.cache.jcache.internal.JCacheRegionFactory
spring.jpa.properties.hibernate.javax.cache.provider=org.ehcache.jsr107.EhcacheCachingProvider

Marking an entity cacheable

Caching is opt-in per entity — nothing is cached just because the provider is configured:

Java
@Entity
@Cacheable
@org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
public class Product {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;
    private BigDecimal price;
}

@Cacheable is the standard JPA annotation opting the entity into the second-level cache at all; @Cache (Hibernate-specific) selects a concurrency strategy — how the cache behaves under concurrent reads and writes to the same entity.

Cache regions and concurrency strategies

Each cacheable entity type gets its own region — a logically separate cache space, individually sized, configured, and evicted:

Concurrency strategy Behavior Fit for
READ_ONLY Entity is cached and never updated after first load; an update on a read-only-cached entity throws Truly immutable reference data (country codes, a finalized historical record)
READ_WRITE Supports updates, using a locking mechanism to keep the cache consistent with concurrent writes The common case — data that's read far more often than it's written, but does change occasionally
NONSTRICT_READ_WRITE Updates the cache without strict locking, accepting a brief window of possible staleness Data where an occasional stale read for a moment after a write is an acceptable trade-off for lower overhead
TRANSACTIONAL Fully transactional, JTA-integrated caching Rare — only when the cache provider and full XA transaction support are already in place

When it helps

  • Rarely-changing, frequently-read reference/lookup data — product categories, country lists, configuration flags, a "plans" table. Many sessions repeatedly reading the exact same small set of rows is the textbook case.
  • Read-heavy entities with an expensive load path — an entity whose query involves several joins or computed columns, read far more often than it changes.

When it hurts

  • Frequently-updated entities — every write to a cached entity has to invalidate (or update) the cache entry across the whole application, adding overhead to every write for a read-side benefit that may not be worth it if reads aren't actually that frequent.
  • Large entities/collections cached wholesale — the cache consumes memory proportional to what's cached; caching something huge for a marginal hit-rate improvement is a poor trade.
  • Data with strict consistency requirements — anything where a cached, slightly-stale read would be a real correctness problem (e.g. real-time inventory counts during checkout) shouldn't be cached at the entity level without very careful thought about the concurrency strategy's staleness window.
Helps Hurts
Read/write ratio Read-heavy Write-heavy
Data volatility Rarely changes Changes often
Size Small-to-moderate, fits comfortably in memory Very large datasets
Consistency needs Tolerant of brief staleness Requires strict, immediate consistency

Common mistakes

  • Enabling the second-level cache application-wide and marking every entity @Cacheable "for performance," including entities that are written to constantly — this adds invalidation overhead to every write without a meaningful read-side win.
  • Choosing READ_ONLY for an entity that does, in fact, get updated occasionally — an update attempt against a read-only-cached entity is a runtime error, not just a missed optimization.
  • Treating the second-level cache as a substitute for proper indexing or query tuning — it helps avoid repeating identical loads across sessions, but does nothing for a genuinely slow, unindexed query the very first time it runs.
  • Forgetting that the second-level cache is per-SessionFactory (per application instance) unless a distributed cache backend is used — in a multi-instance deployment, each instance has its own independent cache, so a write on one instance doesn't automatically invalidate another instance's cached copy without additional distributed-cache configuration.