Question 10 · Section 16

What is the Second-Level Cache and When to Use It

The second-level cache (L2 cache) is a cache at the EntityManagerFactory level, shared between all sessions. Unlike L1 cache, it survives session closures and can be used by mul...

Language versions: English Russian Ukrainian

Overview

The second-level cache (L2 cache) is a cache at the EntityManagerFactory level, shared between all sessions. Unlike L1 cache, it survives session closures and can be used by multiple transactions.

Why: when 100 users simultaneously request the same reference data (e.g., a list of countries), L1 cache is useless - each session has its own cache. L2 cache loads countries ONCE and shares between all.


Junior Level

What is L2 Cache

The second-level cache is a cache shared between sessions. If one session loads an entity, another session can get it from L2 cache without accessing the DB.

Session 1: loads User(1) -> L1 cache + L2 cache
Session 1: closed -> L1 cache deleted, L2 cache remains
Session 2: loads User(1) -> L2 cache (NO DB!)

L1 vs L2 Cache

Characteristic L1 Cache L2 Cache
Level EntityManager EntityManagerFactory
By default Enabled Needs to be enabled
Lifecycle One session Entire application
Sharing No Between sessions
Provider Built-in Ehcache, Hazelcast, Redis

When L2 Cache is Useful

Suitable when:

  • Data rarely changes (reference data, countries, currencies)
  • Many reads, few writes
  • Multiple application instances

Not suitable when:

  • Data changes frequently (orders, logs)
  • Absolute data freshness is required
  • More writes than reads

Middle Level

When NOT to Use L2 Cache

L2 cache does NOT work with:

  • Native SQL queries (Hibernate doesn’t know which entities are affected)
  • Query cache for pagination (cached separately)
  • EntityManager.clear() does not clear L2 cache

Configuration

<!-- Dependency for JCache provider -->
<dependency>
    <groupId>org.hibernate.orm</groupId>
    <artifactId>hibernate-jcache</artifactId>
</dependency>
# application.yml
spring:
  jpa:
    properties:
      hibernate:
        cache:
          use_second_level_cache: true
          region:
            factory_class: org.hibernate.cache.jcache.JCacheRegionFactory
      javax.persistence.sharedCache.mode: ENABLE_SELECTIVE

In Hibernate 5.x, javax.persistence is used. In Hibernate 6+ / Jakarta EE, replace with jakarta.persistence and update factory class paths.

// Marking entity for caching
@Entity
@Cacheable
@org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
public class User {
    @Id
    private Long id;
    private String name;
}

Caching Strategies

READ_ONLY:
- Read-only, data never changes
- Fastest strategy
- For reference data, countries, currencies

READ_WRITE:
- Read and write with synchronization
- Uses soft locks
- For moderately changing data

NONSTRICT_READ_WRITE:
- Read and write without strict synchronization
- May return stale data
- For rarely changing data

TRANSACTIONAL:
- Full transactional consistency
- Requires XA transactions
- For critically important data

Cache Management

Cache cache = entityManager.getEntityManagerFactory().getCache();

// Evict specific entity
cache.evict(User.class, userId);

// Evict all entities of type
cache.evict(User.class);

// Evict everything
cache.evictAll();

Common Mistakes

// Caching frequently changing data
@Cache(usage = CacheConcurrencyStrategy.READ_ONLY)
public class Order { }
// Orders change frequently -> stale data!

// No TTL
// Cache grows infinitely -> memory leak
// Solution: always set TTL

Senior Level

Soft locks - a soft locking mechanism: Hibernate marks the cache entry as “updating”, other threads wait. Unlike hard DB locking, this doesn’t lock the table row.

XA transactions - two-phase transactions ensuring atomicity of operations both in the DB and in the cache.

Eventual consistency - consistency achieved eventually: after an update, data in the cache on different nodes becomes the same not instantly, but after some time.

Invalidation Mechanism

L2 cache invalidation occurs on:
- UPDATE/DELETE of entity -> eviction from cache
- Explicit evict() -> removal from cache
- TTL expiration -> automatic removal
- Cluster replication -> replication between nodes

Clustered Cache

# Hazelcast L2 cache for cluster
spring:
  jpa:
    properties:
      hibernate:
        cache:
          region:
            factory_class: com.hazelcast.hibernate.HazelcastCacheRegionFactory
For multiple application instances:
- Cache replication between nodes (Hazelcast, Redis)
- Eventual consistency (not strong)
- Network overhead during replication
- Must be considered in design

When User is updated on Node 1: (1) Hibernate updates DB;
(2) invalidates entry in L2 cache Node 1;
(3) Hazelcast broadcasts invalidation message to Node 2, 3;
(4) Next request on Node 2 misses cache and loads from DB.

Production Monitoring

// Hibernate Statistics
Statistics stats = entityManagerFactory.unwrap(SessionFactory.class).getStatistics();

// L2 cache metrics
long hitCount = stats.getSecondLevelCacheHitCount();
long missCount = stats.getSecondLevelCacheMissCount();
double hitRatio = (double) hitCount / (hitCount + missCount);

// For production monitoring
// hitRatio > 0.8 - excellent, cache used effectively
// hitRatio < 0.5 - check: are correct entities chosen for caching, is cache size sufficient, is TTL too short

Ehcache Configuration

<!-- ehcache.xml -->
<config>
    <cache alias="com.example.User">
        <heap unit="entries">1000</heap>
        <offheap unit="MB">50</offheap>
        <expiry>
            <ttl unit="minutes">30</ttl>
        </expiry>
    </cache>

    <cache alias="com.example.Country">
        <heap unit="entries">500</heap>
        <expiry>
            <ttl unit="hours">24</ttl>
        </expiry>
    </cache>
</config>

Query Cache vs L2 Cache

Query cache stores: “query X with parameters Y returned entities with IDs [1,2,3]”. L2 cache stores: “entity User(1) = {name: John, …}”. They work together: Query cache gives IDs, L2 cache gives data. Query cache is useless without L2 cache.

Best Practices

READ_ONLY for reference data and constants
READ_WRITE for moderately changing data
Monitor hit/miss ratio
Set TTL for each region
Limit heap size
Invalidate on updates
Clustered cache for multi-node

For frequently changing data
Without hit ratio monitoring
Without TTL and memory limits
Cache for entities with N+1 problem
Using as a replacement for proper architecture

Interview Cheat Sheet

Must know:

  • L2 cache - at EntityManagerFactory level, shared between sessions
  • Needs to be enabled: choose provider (Ehcache, Hazelcast, Redis), configure
  • Strategies: READ_ONLY (reference data), READ_WRITE (moderately changing), NONSTRICT_READ_WRITE, TRANSACTIONAL
  • Useful for: rarely changing data, many reads/few writes, reference data
  • Query cache stores entity IDs, actual data - from L2 cache (useless without L2)
  • Monitoring: hitRatio > 0.8 excellent, < 0.5 - check configuration

Frequent follow-up questions:

  • L1 vs L2 cache? L1 - within session (always enabled), L2 - between sessions (needs enabling)
  • When does L2 cache NOT work? Native SQL queries, query cache for pagination, after EntityManager.clear()
  • How does clustered cache work? Replication between nodes (Hazelcast, Redis), eventual consistency
  • What are soft locks? Soft locking - Hibernate marks entry as “updating”, other threads wait

Red flags (DO NOT say):

  • “I cache all entities indiscriminately” - only cache read-heavy
  • “No TTL - cache grows infinitely” - memory leak
  • “L2 cache for Order/Log” - frequently changing data, cache ineffective
  • “Query cache without L2 cache” - useless, stores only IDs

Related topics:

  • [[9. What is the First-Level Cache in Hibernate]]
  • [[11. How to Configure the Second-Level Cache]]
  • [[1. What is the N+1 Problem and How to Solve It]]
  • [[13. How Does the Flush Mechanism Work in Hibernate]]