The Second-Level Cache
Learn what the Hibernate second-level cache is, how it differs from the first-level cache, and when enabling it via a provider like Ehcache actually pays off.
Introduction
In the previous lesson, you saw the first-level cache: a mandatory, per-Session cache that lasts only as long as that one Session is open. The second-level cache is different in every one of those ways. It is optional, it lives at the SessionFactory level, and it is shared across every Session your application opens.
That difference matters because the first-level cache cannot help two separate requests that both load the same Employee with id 7 — each request gets its own Session, so each one hits the database. A second-level cache can serve the second request straight from memory instead.
- How the second-level cache differs from the first-level cache
- How to enable a cache provider like Ehcache
- How to mark an entity as cacheable with @Cacheable
- Cache concurrency strategies and when to use each
- When enabling the second-level cache is actually worth it
First-Level vs Second-Level
It helps to see the two side by side before touching any configuration.
| Aspect | First-Level Cache | Second-Level Cache |
|---|---|---|
| Scope | A single Session | The whole SessionFactory |
| Lifetime | Ends when the Session closes | Survives across many Sessions |
| Enabled by default | Yes, always on | No, opt-in |
| Requires a provider | No | Yes (e.g. Ehcache, Caffeine) |
| Typical use | Avoiding duplicate loads in one unit of work | Avoiding repeated loads of rarely-changing data across requests |
How It Works
When the second-level cache is enabled for an entity, Hibernate stores a disassembled, cache-provider-friendly representation of that entity keyed by its identifier. The next time any Session asks for that entity by id, Hibernate checks the second-level cache first. Only on a miss does it fall through to a SQL query against the database.
The second-level cache primarily speeds up load-by-id operations (session.get(), and association fetches by foreign key). It does not automatically cache the results of arbitrary HQL queries — that is a separate, optional feature called the query cache.
Enabling a Cache Provider
Hibernate does not ship its own cache implementation — it delegates to a caching provider through the JCache (JSR-107) API or a native integration. Ehcache is the most common choice. First, add the dependency, then turn on second-level caching in your Hibernate configuration.
<dependency> <groupId>org.hibernate.orm</groupId> <artifactId>hibernate-jcache</artifactId> <version>6.4.4.Final</version></dependency><dependency> <groupId>org.ehcache</groupId> <artifactId>ehcache</artifactId> <version>3.10.8</version></dependency><property name="hibernate.cache.use_second_level_cache">true</property><property name="hibernate.cache.region.factory_class">jcache</property><property name="hibernate.javax.cache.provider">org.ehcache.jsr107.EhcacheCachingProvider</property><property name="hibernate.cache.use_query_cache">false</property>That last property, use_query_cache, is off by default here on purpose — enable it separately only if you plan to cache specific HQL query results, since query caching has its own overhead and invalidation rules.
Caching an Entity
Enabling the provider is not enough by itself — you must opt each entity into caching individually with @Cacheable and tell Hibernate which concurrency strategy to use.
import javax.persistence.*;import org.hibernate.annotations.Cache;import org.hibernate.annotations.CacheConcurrencyStrategy;
@Entity@Table(name = "products")@Cacheable@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)public class Product {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id;
private String name; private Double price;
// Getters and setters}SessionFactory factory = new Configuration().configure().buildSessionFactory();
try (Session session1 = factory.openSession()) { Product p = session1.get(Product.class, 1L); // Hits the database System.out.println(p.getName());}
try (Session session2 = factory.openSession()) { Product p = session2.get(Product.class, 1L); // Served from the second-level cache System.out.println(p.getName());}Click Run to see what this code prints.
Cache Concurrency Strategies
The usage attribute on @Cache tells Hibernate how carefully it needs to guard against stale or inconsistent cached data. Pick the weakest strategy that is still safe for the data in question.
| Strategy | Behavior | Good For |
|---|---|---|
| READ_ONLY | Cached once, never updated. An update throws an exception if attempted. | Reference data that truly never changes (country codes, currencies) |
| READ_WRITE | Uses locking to keep the cache consistent with concurrent writes. | Data that is read often but occasionally updated |
| NONSTRICT_READ_WRITE | Weaker consistency, lower overhead; brief staleness is possible after a write. | Data where an occasional stale read is acceptable |
| TRANSACTIONAL | Fully transactional, requires a JTA-aware cache provider. | Strict consistency requirements in a JTA environment |
When It Is Worth It
The second-level cache is a real win for data that is read far more often than it is written, and where a small window of staleness is tolerable — think product catalogs, lookup/reference tables, or configuration entities. It is a poor fit for data that changes constantly, since the cache invalidation overhead can end up costing more than it saves.
Do not enable second-level caching speculatively. Turn on SQL logging, look for repeated identical queries across requests, and cache only the entities where that pattern actually shows up.
Common Mistakes
- Enabling the second-level cache for frequently-updated entities, which adds overhead without a real payoff.
- Forgetting that @Cacheable alone does nothing without hibernate.cache.use_second_level_cache=true and a configured provider.
- Assuming the second-level cache speeds up arbitrary HQL queries — it only accelerates id-based lookups unless the separate query cache is also enabled.
- Using READ_ONLY on an entity that does, in fact, get updated, causing runtime exceptions.
- Never invalidating stale entries after bulk updates or native SQL that bypasses Hibernate.
Best Practices
- Enable second-level caching selectively, entity by entity, not globally by default.
- Start with READ_ONLY or READ_WRITE and only reach for weaker or stronger strategies with a specific reason.
- Cache small, rarely-changing reference/lookup data first — it is the highest-value, lowest-risk case.
- Watch your SQL logs before and after enabling caching to confirm it is actually reducing queries.
- Be cautious with bulk updates (UPDATE ... HQL, native SQL) since they can bypass the cache and leave it stale.
Frequently Asked Questions
No. Unlike the first-level cache, it is entirely opt-in and requires both a configured cache provider and @Cacheable on each entity you want cached.
No, it stores a disassembled representation of the entity's state, which the cache provider can serialize and manage more efficiently than a live Java object graph.
The second-level cache speeds up loading entities by id. The query cache is a separate, opt-in feature that caches the result sets of specific HQL/Criteria queries.
Yes, several JCache-compliant providers work with Hibernate, including Redis-backed ones, as long as they implement the JSR-107 API Hibernate expects.
Key Takeaways
- The second-level cache is SessionFactory-wide and shared across all Sessions, unlike the per-Session first-level cache.
- It requires a cache provider (like Ehcache) and is off by default.
- Entities opt in with @Cacheable and @Cache(usage = ...).
- Concurrency strategies (READ_ONLY, READ_WRITE, NONSTRICT_READ_WRITE, TRANSACTIONAL) trade off consistency against overhead.
- It is best suited to read-heavy, rarely-changing data — measure before enabling it broadly.
Summary
The second-level cache extends caching beyond a single Session, letting rarely-changing entities be served from memory across your whole application instead of hitting the database every time. It is powerful but not free — enable it deliberately, entity by entity, based on actual read/write patterns rather than by default.