Second-Level Cache
AdvancedThe 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).
<!-- 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`.
@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.
// 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 tablesKey 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 AriaWhat is the difference between the first-level and second-level cache in Hibernate?
Why do bulk JPQL UPDATE statements cause stale data in the second-level cache?
What is the difference between READ_ONLY and READ_WRITE cache concurrency strategy?
When would you NOT use the Hibernate query cache?
How would you monitor the effectiveness of the second-level cache?
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.