Home/Learn/Hibernate & JPA/Second-Level Cache

Second-Level Cache

Advanced
Performance

The second-level cache (Ehcache, Caffeine, Infinispan) caches entities across sessions; annotate entities with @Cache and configure region expiry to avoid stale reads.

Overview

Hibernate's **first-level cache** (the persistence context) is per-session and per-transaction — it is discarded when the session closes. The **second-level cache (L2C)** is shared across all sessions within the same SessionFactory, making cached entities available to subsequent requests without hitting the database. L2C stores entities by primary key. Common providers are **Ehcache** (in-process), **Caffeine**, and **Infinispan** (distributed). The **query cache** is a separate, related feature that caches the result IDs of a specific JPQL/HQL query. Both caches require careful configuration: stale data is the main risk — any direct DB update bypassing Hibernate will not invalidate the cache.

Enabling L2C with Ehcache (Spring Boot)

Add `hibernate-jcache` and `ehcache` (or `caffeine`) as dependencies. Configure the provider in `application.properties`. Then annotate individual entities with `@Cache` — only entities explicitly annotated are cached; this is intentional (opt-in to avoid caching sensitive data by accident).

Spring Boot — enable L2C with Ehcache
<!-- pom.xml -->
<dependency>
  <groupId>org.hibernate.orm</groupId>
  <artifactId>hibernate-jcache</artifactId>
</dependency>
<dependency>
  <groupId>org.ehcache</groupId>
  <artifactId>ehcache</artifactId>
  <classifier>jakarta</classifier>
</dependency>

# application.properties
spring.jpa.properties.hibernate.cache.use_second_level_cache=true
spring.jpa.properties.hibernate.cache.region.factory_class=jcache
spring.jpa.properties.hibernate.javax.cache.provider=\
  org.ehcache.jsr107.EhcacheCachingProvider
spring.jpa.properties.hibernate.cache.use_query_cache=true  # optional

@Cache on Entities and Collections

Opt individual entities and collections into the L2C. `CacheConcurrencyStrategy.READ_WRITE` is safe for entities that are updated — Hibernate uses soft locks to prevent serving stale data during writes. `READ_ONLY` is faster for immutable reference data (countries, categories). Associations (`@OneToMany`) must be cached separately with their own `@Cache`.

Hibernate — @Cache annotation and Ehcache region
@Entity
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE,    // or READ_ONLY for immutable data
       region = "product-cache")                        // named region for TTL config
@Table(name = "products")
public class Product {
    @Id Long id;
    String name;
    BigDecimal price;

    @OneToMany(mappedBy = "product")
    @Cache(usage = CacheConcurrencyStrategy.READ_WRITE) // cache the collection too
    List<Review> reviews;
}

// Ehcache region config (ehcache.xml)
<cache alias="product-cache">
    <expiry>
        <ttl unit="minutes">60</ttl>
    </expiry>
    <heap units="entries">10000</heap>
</cache>

// Verify cache hits with Hibernate statistics
SessionFactory sf = em.getEntityManagerFactory().unwrap(SessionFactory.class);
Statistics stats = sf.getStatistics();
stats.setStatisticsEnabled(true);
log.info("L2C hit ratio: {}", stats.getSecondLevelCacheHitCount());

Cache Eviction, Stale Data Risks, and Query Cache

The biggest L2C risk is **stale data**: any write that bypasses Hibernate (bulk JPQL UPDATE, native SQL, direct DB tool edit) does not invalidate the cache. The **query cache** stores result sets (as ID arrays) keyed by query + params. It is invalidated when any entity in the queried table is modified — making it only useful for slow-changing reference data. Always test L2C in staging with realistic write loads.

Hibernate — cache eviction and query cache pitfalls
// Manual cache eviction
EntityManagerFactory emf = ...;
emf.getCache().evict(Product.class, productId);    // single entity
emf.getCache().evictAll();                         // nuclear option

// Bulk update bypasses L2C — evict manually after
@Modifying @Transactional
@Query("UPDATE Product p SET p.price = p.price * 1.1 WHERE p.category = :cat")
int raisePrices(@Param("cat") String category);

// After bulk update: must evict affected entities
emf.getCache().evict(Product.class);   // evict all Product entries

// Query cache — only for reference/static data
@QueryHints(@QueryHint(name = "org.hibernate.cacheable", value = "true"))
List<Country> findAll();

// Query cache invalidation is aggressive:
// ANY change to the countries table (even unrelated) invalidates ALL cached queries
// on that table — making the query cache counter-productive on frequently-updated tables

Key Points to Remember

  • 1L2C is shared across all sessions in the same SessionFactory — unlike the per-session first-level cache
  • 2Entities must opt-in with @Cache — only annotated entities are cached
  • 3READ_WRITE strategy uses soft locks to prevent stale reads during concurrent writes
  • 4Bulk JPQL/SQL updates bypass L2C — manually evict affected regions after bulk writes
  • 5Query cache stores result IDs; invalidated aggressively on any table write — only useful for static data
  • 6Monitor L2C hit ratio with Hibernate Statistics; enable via setStatisticsEnabled(true)

Interview Questions

Sign in to ask Aria
1

What is the difference between the first-level and second-level cache in Hibernate?

EasyInfosys
2

Why do bulk JPQL UPDATE statements cause stale data in the second-level cache?

HardThoughtWorks
3

What is the difference between READ_ONLY and READ_WRITE cache concurrency strategy?

MediumOracle
4

When would you NOT use the Hibernate query cache?

HardBooking.com
5

How would you monitor the effectiveness of the second-level cache?

MediumAtlassian

Ask Aria about Second-Level Cache

Your personal AI tutor — ask anything about this concept

Revision Status

Personal Notes

Sign in to save personal notes for this topic.

Discussion

Sign in to join the discussion.

Loading discussion…