Hibernate Caching — L2 Stale Data from Native SQL Batch
20-minute-old prices from L2 cache after native SQL batch - debug stale entities, empty query cache, and high memory with eviction strategies..
20+ years shipping production Java in banking & fintech. Lessons pulled from things that broke in production.
- ✓Solid grasp of fundamentals
- ✓Comfortable reading code examples
- ✓Basic production concepts
- Hibernate L1 cache is the Session-level identity map, always on, cannot be disabled
- L2 cache is the SessionFactory-level shared cache, optional, requires explicit configuration with a provider
- Query cache stores query result IDs, not entities; entity cache must be enabled for efficiency
- Performance: L1 offers near-zero overhead, L2 adds 0.5-2ms per cache hit depending on provider
- Production pitfall: L2 cache without proper invalidation leads to stale data when external updates bypass Hibernate
Hibernate Caching is a mechanism within the Hibernate ORM (Object-Relational Mapping) framework that stores frequently accessed data in memory to reduce the number of database round-trips. It operates at multiple levels: the first-level cache (session-level, always enabled) caches entities within a single Hibernate session, while the second-level cache (session-factory-level, optional) shares cached data across sessions and can be configured with providers like Ehcache, Redis, or Infinispan.
Query caches additionally store results of HQL or Criteria queries to avoid re-execution. This layered architecture ensures that repeated requests for the same data are served from memory rather than hitting the database, significantly improving application performance and scalability.
Think of Hibernate Caching — First and Second Level as a two-tier storage system in a library. The First Level Cache is like the desk where you're currently working; any book you've already opened is there for instant access. The Second Level Cache is like a shared cart in the hallway that multiple librarians can use; if you don't have the book on your desk, you check the cart before making the long trip back to the main archives (the database).
Hibernate Caching — First and Second Level is a fundamental concept in Java persistence development. It serves as a mechanism to minimize expensive database hits by keeping frequently accessed entities in memory. Understanding the distinction between these layers is critical for building high-performance, scalable Spring Boot applications.
In this guide, we'll break down exactly what Hibernate Caching is, why the two-level architecture was designed to balance isolation and shared access, and how to configure them correctly in production-grade environments. We will explore how the Persistence Context manages state and how external providers like Ehcache 3 or Hazelcast plug into the Hibernate lifecycle to provide distributed caching capabilities.
By the end, you'll have both the conceptual understanding and practical code examples to implement Hibernate caching strategies with confidence, ensuring your 'io.thecodeforge' microservices handle high-traffic loads with minimal latency.
What Is Hibernate Caching — First and Second Level and Why Does It Exist?
The First Level Cache (L1) is mandatory and associated with the Session object. It ensures that within a single transaction, the same entity is not fetched from the database multiple times. This is also known as the 'Persistence Context' or 'Identity Map' pattern.
The Second Level Cache (L2) is optional and exists at the SessionFactory level, shared across all sessions. It solves the problem of cross-session data redundancy and reduces the load on the database for read-heavy applications. While L1 is short-lived (lasting only as long as the transaction), L2 is long-lived and typically managed by a third-party provider. This architecture allows Hibernate to balance data consistency (L1) with global performance optimization (L2).
package io.thecodeforge.config; import org.springframework.boot.autoconfigure.orm.jpa.HibernatePropertiesCustomizer; import org.springframework.context.annotation.Configuration; import java.util.Map; /** * io.thecodeforge production-grade L2 Cache Configuration * Strategy: JSR-107 (JCache) with Ehcache 3 for distributed-ready caching. */ @Configuration public class HibernateCacheConfig implements HibernatePropertiesCustomizer { @Override public void customize(Map<String, Object> hibernateProperties) { // 1. Explicitly enable the Second Level Cache hibernateProperties.put("hibernate.cache.use_second_level_cache", "true"); // 2. Enable Query Cache (Caches results of JPQL/Criteria queries) hibernateProperties.put("hibernate.cache.use_query_cache", "true"); // 3. Define the Region Factory using JCache (Standardized via JSR-107) hibernateProperties.put("hibernate.cache.region.factory_class", "org.hibernate.cache.jcache.internal.JCacheRegionFactory"); // 4. Point to the specific Ehcache XML configuration hibernateProperties.put("hibernate.javax.cache.uri", "classpath:ehcache.xml"); // 5. Missing identifiers strategy: generate exception if entity is not found in cache hibernateProperties.put("hibernate.cache.use_minimal_puts", "true"); } }
Common Mistakes and How to Avoid Them
When learning Hibernate Caching, most developers fall into the trap of over-caching or failing to manage the cache lifecycle. A frequent 'gotcha' is performing bulk updates directly via SQL (Native or JPQL); this bypasses the Hibernate cache, leading to 'stale data' where the cache holds old values while the database has been updated.
At io.thecodeforge, we advocate for the 'Cache-Aside' or 'Read-Through' mindset. Another mistake is enabling L2 cache for highly volatile data. If an entity changes every second, the overhead of invalidating the L2 cache across a cluster (if using Hazelcast/Redis) will actually make your application slower than hitting the database directly.
package io.thecodeforge.entities; import jakarta.persistence.*; import org.hibernate.annotations.Cache; import org.hibernate.annotations.CacheConcurrencyStrategy; import java.math.BigDecimal; /** * io.thecodeforge best practice: Entity-level caching configuration. */ @Entity @Table(name = "forge_products") @Cacheable // NONSTRICT_READ_WRITE is ideal if your app tolerates occasional stale data // and updates aren't happening concurrently in a heavy way. @Cache(usage = CacheConcurrencyStrategy.READ_WRITE, region = "product_cache") public class Product { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(nullable = false) private String sku; private String name; private BigDecimal price; // Standard Getters/Setters ignored for brevity /** * Production-grade Tip: Use versioning to help Hibernate manage * optimistic locking and cache invalidation correctly. */ @Version private Long version; }
Query Cache – What It Really Caches and When to Use It
The Query Cache stores the identifiers of entities returned by a query, not the entity data itself. When a cached query is re-executed, Hibernate loads entity identifiers from the cache, then looks up each entity in the L2 entity cache (or DB if missing). This means Query Cache is only useful when Entity Cache is also enabled for the same entities.
Common misconception: enabling hibernate.cache.use_query_cache alone will reduce DB hits. It won't – you'll still hit the DB for each entity if L2 entity cache is off. The real gain comes from combining both: query cache avoids the SQL parse & fetch, entity cache avoids the row load.
Production reality: Query Cache works best for low-churn data like reference tables (countries, status codes). For high-volume transactional data, the overhead of invalidation often outweighs the benefit.
package io.thecodeforge.repositories; import io.thecodeforge.entities.Product; import jakarta.persistence.Query; import org.springframework.stereotype.Repository; import jakarta.persistence.EntityManager; import jakarta.persistence.PersistenceContext; import java.util.List; @Repository public class ProductRepository { @PersistenceContext private EntityManager entityManager; public List<Product> findActiveProducts() { // Enable query cache for this specific query Query query = entityManager.createQuery("SELECT p FROM Product p WHERE p.active = true"); query.setHint("org.hibernate.cacheable", true); query.setHint("org.hibernate.cacheRegion", "query.product.active"); return query.getResultList(); } }
- Query cache: stores result set IDs → avoids SQL execution
- Entity cache: stores the actual book (entity data)
- If entity cache is off, query cache still triggers N+1 DB hits per ID
- Always enable both for maximum benefit
Cache Concurrency Strategies – Choosing the Right Lock Mode
Hibernate provides four CacheConcurrencyStrategy options: READ_ONLY, READ_WRITE, NONSTRICT_READ_WRITE, and TRANSACTIONAL. Each has a different trade-off between consistency and throughput.
- READ_ONLY: assumes data never changes. Fastest, no locking. Exception thrown if any mutation attempted.
- READ_WRITE: uses soft locks (a version field or timestamp) to ensure one writer at a time. Good balance for moderate contention.
- NONSTRICT_READ_WRITE: no locks – writers update cache directly. Readers may see stale data briefly. Suitable for data that can tolerate eventual consistency.
- TRANSACTIONAL: uses JTA transactions for atomic updates. Requires a cache provider that supports JTA (e.g., Infinispan). Highest consistency, highest overhead.
The wrong choice leads to performance nightmares or runtime errors. For an e-commerce pricing service with frequent updates, READ_WRITE with a version column is the safest bet.
package io.thecodeforge.entities; import jakarta.persistence.*; import org.hibernate.annotations.Cache; import org.hibernate.annotations.CacheConcurrencyStrategy; @Entity @Cacheable @Cache(usage = CacheConcurrencyStrategy.TRANSACTIONAL, region = "promotion_cache") public class Promotion { @Id @GeneratedValue private Long id; private String rule; @Version private Long version; // required for READ_WRITE, optional for TRANSACTIONAL // getters/setters }
Production Monitoring, Eviction, and Cache Tuning
Once L2 cache is in production, you need to monitor hit ratios, eviction policies, and memory usage. Enable Hibernate statistics via hibernate.generate_statistics=true (or spring.jpa.properties.hibernate.generate_statistics=true). Then register a StatisticsImplementor bean to log cache metrics periodically.
- SecondLevelCacheHitCount: ratio tells you cache effectiveness
- SecondLevelCacheMissCount: high miss rate means wrong caching or TTL too short
- EntityLoadCount vs EntityFetchCount: compare cached vs DB loads
Eviction policies (LFU, LRU, TTL) should be configured per region. For volatile entities like user sessions, set TTL to minutes. For static catalog data, use size-limited LFU cache with hours-long TTL.
Common tuning mistake: default configs from cache providers are often too forgiving. Ehcache's default max heap entries is 10,000 per region – that's enough to consume gigabytes if each entity is large. Set explicit limits.
<config xmlns='http://www.ehcache.org/v3' xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance' xsi:schemaLocation='http://www.ehcache.org/v3 http://www.ehcache.org/schema/ehcache-core-3.0.xsd'> <cache alias="product_cache"> <key-type>java.lang.Long</key-type> <value-type>io.thecodeforge.entities.Product</value-type> <resources> <heap unit="entries">5000</heap> <offheap unit="MB">50</offheap> </resources> <expiry> <ttl unit="minutes">30</ttl> </expiry> </cache> <cache alias="query.product.active"> <key-type>java.lang.String</key-type> <value-type>java.util.ArrayList</value-type> <resources> <heap unit="entries">100</heap> </resources> <expiry> <ttl unit="seconds">60</ttl> </expiry> </cache> </config>
hibernate.cache.use_minimal_puts=true. It reduces cache write operations by checking if a put would overwrite an existing entry, saving cycles in high-throughput scenarios.use_minimal_puts to reduce write overhead.Second-Level Cache: The 10x Query Killer (If You Configure It Right)
Second-level cache (L2) is where Hibernate stores entity data across sessions. The first-level cache is session-scoped and dies when your session closes — useless for read-heavy workloads. L2 lives in a shared region and survives across transactions. It's the difference between hammering your database every request and serving data from memory.
Why bother? Because database round-trips are expensive. L2 reduces latency, cuts load on your DB, and keeps your app responsive under pressure. But it's not magic. L2 only works for entities marked as cacheable. You must configure a cache provider (EHCache, Redis, Infinispan) and tune eviction policies or you'll fill memory with stale data and kill performance.
The real trap: L2 caches by entity ID. Queries that filter on non-ID columns bypass it entirely. Use the query cache for those, or you're just burning RAM.
// io.thecodeforge — java tutorial import org.hibernate.SessionFactory; import org.hibernate.cfg.Configuration; import javax.persistence.Cacheable; import org.hibernate.annotations.Cache; import org.hibernate.annotations.CacheConcurrencyStrategy; @Entity @Cacheable // step 4: mark entity as cacheable @Cache(usage = CacheConcurrencyStrategy.READ_WRITE) // step 5: pick concurrency strategy public class Invoice { @Id private Long id; private String invoiceNumber; private double amount; } public class ConfigureL2Cache { public static void main(String[] args) { Configuration config = new Configuration(); config.setProperty("hibernate.cache.use_second_level_cache", "true"); // step 3 config.setProperty("hibernate.cache.region.factory_class", "org.hibernate.cache.ehcache.EhCacheRegionFactory"); // step 2: provider config.setProperty("net.sf.ehcache.configurationResourceName", "/ehcache.xml"); SessionFactory factory = config.buildSessionFactory(); // First call: loads from DB, stores in L2 Invoice inv1 = factory.openSession().get(Invoice.class, 1L); // Second call: loads from L2 cache, zero SQL Invoice inv2 = factory.openSession().get(Invoice.class, 1L); System.out.println("Same object? " + (inv1 == inv2)); // false (different sessions) System.out.println("Same data? " + inv1.getAmount() == inv2.getAmount()); // true } }
Query Cache: Cache Results by Parameters, Not IDs
Query cache stores the results of HQL, JPQL, or Criteria queries — keyed by query string and parameter values. It doesn't store the actual entity data. Instead, it holds a list of entity IDs (if the query returns entities) or raw data (if scalar). When the same query runs again, Hibernate checks the L2 cache for those IDs. If the entities are in L2, no DB call. If not, it hits the DB anyway.
Why use it? When you have expensive queries that are executed repeatedly with the same parameters — think dashboards, reporting, or lookup tables. Without query cache, every request re-executes the SQL. With it, you skip the DB entirely if L2 is warm.
The kicker: stale data is aggressive. Query cache invalidates on any insert, update, or delete on the cached entity types — not just the rows you touched. If your app has frequent writes to cached entities, query cache becomes a performance incinerator. Use it only for read-mostly data.
// io.thecodeforge — java tutorial import org.hibernate.Session; import org.hibernate.query.Query; public class QueryCacheExample { public static void main(String[] args) { Session session = factory.openSession(); // Enable query cache per query Query<Invoice> query = session.createQuery( "FROM Invoice WHERE amount > :minAmount", Invoice.class); query.setParameter("minAmount", 1000.0); query.setCacheable(true); // THIS IS THE KEY: enables query caching query.setCacheRegion("invoice.amount.region"); // optional: name the region // First execution: hits DB, populates L2 IDs List<Invoice> results1 = query.list(); // Second execution (same params): no SQL, reads IDs from query cache, entities from L2 List<Invoice> results2 = query.list(); System.out.println("Query results count: " + results1.size()); session.close(); } }
First-Level Cache: The Session-Scoped Liar You Trust Too Much
Every Hibernate Session comes with a first-level cache. It's always on. You can't turn it off. And it will silently serve you stale data if you hold Sessions open too long. This isn't a performance feature — it's identity protection within a single unit of work. Hibernate guarantees that two loads of the same ID in the same Session return the same Java object reference. That's useful for preventing infinite loops in lazy loading and keeping your ORM mapping sane.
But here's where it bites you: if you query the same entity twice via a JPQL or Criteria query, the first query hits the database, stores the result in the persistence context, and the second query — even with different parameters — might still return managed objects from the first-level cache. This isn't a bug; it's the isolation contract. The fix? Never let a Session outlive a single HTTP request in a web app. Detach entities explicitly or close the Session. First-level cache is a transaction-scoped tool, not a replacement for second-level caching.
// io.thecodeforge — java tutorial import org.hibernate.Session; import org.hibernate.SessionFactory; public class FirstLevelCacheTrap { public void dangerousPattern(SessionFactory sf) { Session session = sf.openSession(); try { // First load — hits DB, entity now managed User u1 = session.get(User.class, 42L); u1.setEmail("old@email.com"); // Modified in memory // Second load — returns SAME object from persistence context // Database may have "new@email.com" — you get the stale "old" User u2 = session.get(User.class, 42L); System.out.println(u2.getEmail()); // Prints "old@email.com" } finally { session.close(); } } }
Second-Level Cache: Don't Stick @Cache on Everything — You'll Regret It
The second-level cache is a process-wide cache shared across Sessions. It can turn a 10-query N+1 disaster into a single database hit. But slapping @Cache(usage = READ_WRITE) on every entity is a rookie move that will kill your throughput. The WHY: second-level cache serializes entity state into a cache region. If your entity has @ManyToMany collections or is updated frequently across threads, you'll spend more time invalidating cache lines than fetching from the database.
Only cache entities that are read often, written rarely, and have a stable schema. Entities like StatusCode, Country, or reference data are perfect. High-churn entities like Transaction or AuditLog are poison for the cache — they cause constant eviction and lock contention. Choose your cache concurrency strategy carefully: READ_ONLY for immutable data, READ_WRITE for mostly-read data with rare updates, and NONSTRICT_READ_WRITE if you can tolerate eventual consistency. Never use TRANSACTIONAL unless your cache provider supports JTA transactions, which most production apps don't need.
// io.thecodeforge — java tutorial import org.hibernate.annotations.Cache; import org.hibernate.annotations.CacheConcurrencyStrategy; import javax.persistence.*; @Entity @Cache(usage = CacheConcurrencyStrategy.READ_ONLY) // Immutable after insert public class Country { @Id private String code; // "US", "DE", "JP" private String name; // Getters only — no setters for truly immutable public String getCode() { return code; } public String getName() { return name; } } // Usage // Country c = session.get(Country.class, "US"); // First call: DB hit // Country c2 = session.get(Country.class, "US"); // Second call: L2 cache hit // Country c3 = session.get(Country.class, "US"); // Third call: L2 cache hit // No SQL seen after the first fetch within cache TTL
Why Hibernate Cache Fails Under High Concurrency (And How to Fix It)
Most teams discover Hibernate caching bottlenecks only after production paging slows to a crawl. The problem isn't the cache itself — it's default isolation and write-behind behavior. Under high concurrency, stale reads from first-level cache cause phantom updates, while second-level cache grows unbounded without eviction policies. The fix starts with understanding that Hibernate's caching is session-scoped, not transaction-scoped. When two threads modify the same entity in different sessions, the second session may evict the first session's changes silently. Use explicit lock modes (OPTIMISTIC_FORCE_INCREMENT or PESSIMISTIC_WRITE) on high-contention entities to guarantee consistency. Pair with a bounded cache region size and a TTL equal to your business tolerance for stale data. Avoid @Cache on collections with frequent inserts — they inflate region size and trigger full evictions.
// io.thecodeforge — java tutorial import org.hibernate.annotations.Cache; import org.hibernate.annotations.CacheConcurrencyStrategy; @Entity @Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE) public class Product { @Id private Long id; private int stock; public void decrementStock(int qty) { this.stock -= qty; } } // Bounded region in ehcache.xml: // <cache name="Product" maxEntriesLocalHeap="1000" timeToLiveSeconds="60"/>
The Hidden Cost of Query Cache: When Parameters Trick You Into Cache Misses
Query cache doesn't cache the query result — it caches a list of entity IDs matching the query parameters. If you use position-based parameters (?) instead of named parameters, Hibernate caches the SQL string itself as the key. Two identical queries with different parameter order produce different cache keys, causing misses. Worse, query cache invalidates on any insert, update, or delete on any table involved in the query — even unrelated rows. This makes query cache essentially useless for write-heavy tables. Use it only for read-mostly, static reference data queries with named parameters and fixed parameter order. Combine with second-level cache for the entity fetch step, otherwise the miss penalty doubles. Never enable query cache on queries involving large result sets or joins with high-churn tables.
// io.thecodeforge — java tutorial // BAD: positional params cause cache key mismatch Query q = session.createQuery("from Product where price > ? and category = ?"); q.setParameter(0, 50.0); q.setParameter(1, "Electronics"); q.setCacheable(true); // GOOD: named params, same cache key every time Query q2 = session.createQuery("from Product where price > :minPrice and category = :cat"); q2.setParameter("minPrice", 50.0); q2.setParameter("cat", "Electronics"); q2.setCacheable(true);
Introduction
Hibernate caching is a powerful performance optimization layer that reduces database round-trips by storing frequently accessed data in memory. Understanding caching is critical for Java enterprise applications where latency and database load directly impact user experience. Hibernate provides a three-tier caching architecture: first-level cache (mandatory, per-session), second-level cache (optional, session-factory-scoped), and query cache (optional, caches query results). Each tier serves a distinct purpose and comes with its own trade-offs. Without proper configuration, developers often face stale data, memory bloat, or cache misses that degrade performance instead of improving it. The key insight is that caching works best for read-heavy, rarely-updated data. Before jumping into implementation, you must evaluate your application's access patterns—caching write-heavy entities or frequently mutated data leads to concurrency nightmares. This guide strips away the hype and gives you a production-ready foundation for Hibernate caching with Java and Maven.
// io.thecodeforge — java tutorial // Hibernate caching tiers at a glance public class CachingIntro { public static void main(String[] args) { System.out.println("First-Level Cache: Session-scoped, always on"); System.out.println("Second-Level Cache: Factory-scoped, optional"); System.out.println("Query Cache: Parameter-based result caching"); // Rule: Cache read-heavy, rarely-updated data String rule = "50% read-to-write ratio? Don't cache."; System.out.println(rule); } }
1. Overview
This section provides a structured roadmap for implementing Hibernate caching in a Java project. We'll start by creating a Maven project, then add the necessary dependencies—Hibernate Core, Hibernate JPA, Ehcache or Redis as a second-level cache provider, and JDBC drivers. The Maven project structure ensures reproducible builds and dependency management. After setup, we configure caching through Hibernate properties in persistence.xml or application.properties. The critical decision is choosing between Ehcache (simple, embedded) and Redis (distributed, production-grade). For most applications, Ehcache suffices for single-node deployments, while Redis excels in clustered environments. We'll also touch on entity-level @Cache annotations, region factories, and query cache parameters. The goal is to give you a reusable template that eliminates guesswork. Every configuration option directly impacts cache hit ratios and memory consumption—we'll show you the defaults and the tuning levers. Follow this overview step by step to avoid the common pitfall of enabling caching without understanding its lifecycle implications.
// io.thecodeforge — java tutorial // Maven setup overview for Hibernate caching public class SetupOverview { public static void main(String[] args) { // Step 1: Maven project with pom.xml System.out.println("GroupId: com.thecodeforge"); System.out.println("ArtifactId: hibernate-caching"); System.out.println("Dependencies: hibernate-core, ehcache, mysql"); // Step 2: Configure persistence.xml String config = "hibernate.cache.region.factory_class = jcache"; System.out.println(config); // Step 3: Annotate entities with @Cache System.out.println("@Cache(usage = READ_WRITE)"); } }
The Stale Price Experiment: When L2 Cache Cost a Million
- Never assume L2 cache knows about out-of-band DB changes.
- Use region-based eviction or cache invalidation hooks for external updates.
- For frequently updated entities, consider READ_WRITE strategy with short TTLs.
sessionFactory.getCache().evictEntityRegion(MyEntity.class) and verify cache TTL config.-Dnet.sf.ehcache.statistics=true to monitor hit/miss ratios.hibernate.cache.use_second_level_cache and hibernate.cache.region.factory_class are set. Also ensure entity is annotated with @Cacheable and @Cache.Check cache hit/miss stats: call `Statistics stats = sessionFactory.getStatistics(); stats.logSummary();` (needs stats enabled)Verify if cache TTL is too long: inspect provider config (ehcache.xml).Enable Hibernate logging: `spring.jpa.show-sql=true` and `logging.level.org.hibernate.cache=TRACE`Run a direct entity load to see if entity cache is populated.@Cache(usage=READ_ONLY) to entity and ensure hibernate.cache.use_query_cache=true.Monitor heap usage: `jstat -gc <pid> 1s`Check if entities are being loaded without eviction in large batches.session.flush(); session.clear(); every 100 records.Check logs for `HHH000172:` message confirming cache region factory initialization.Ensure entity is annotated with `@Cacheable` and `@Cache(usage=...)`.| Feature | First Level Cache (L1) | Second Level Cache (L2) |
|---|---|---|
| Scope | Session Level (Private to one thread) | SessionFactory Level (Shared across threads) |
| Availability | Mandatory (Always on, cannot disable) | Optional (Must be explicitly enabled) |
| Lifecycle | Ends with the Session/Transaction | Ends with the SessionFactory/Application |
| Performance | Extremely Fast (Local HashMap) | Fast (Provider-dependent, often off-heap) |
| Stale Data Risk | Low (Short-lived isolation) | Higher (Requires robust eviction policies) |
| Data Storage | Stores entity objects | Stores hydrated state (deconstructed data) |
| File | Command / Code | Purpose |
|---|---|---|
| io | /** | What Is Hibernate Caching |
| io | /** | Common Mistakes and How to Avoid Them |
| io | @Repository | Query Cache – What It Really Caches and When to Use It |
| io | @Entity | Cache Concurrency Strategies – Choosing the Right Lock Mode |
| src | | Production Monitoring, Eviction, and Cache Tuning | |
| ConfigureL2Cache.java | @Entity | Second-Level Cache |
| QueryCacheExample.java | public class QueryCacheExample { | Query Cache |
| FirstLevelCacheTrap.java | public class FirstLevelCacheTrap { | First-Level Cache |
| SecondLevelCacheExample.java | @Entity | Second-Level Cache: Don't Stick @Cache on Everything |
| ConcurrencyFix.java | @Entity | Why Hibernate Cache Fails Under High Concurrency (And How to |
| QueryCacheTrap.java | Query q = session.createQuery("from Product where price > ? and category = ?"); | The Hidden Cost of Query Cache |
| CachingIntro.java | public class CachingIntro { | Introduction |
| SetupOverview.java | public class SetupOverview { | 1. Overview |
Key takeaways
Common mistakes to avoid
4 patternsNot clearing the Session in large batch jobs
session.flush() and session.clear() every 50-100 records to free L1 cache.Forgetting to enable Query Caching explicitly
hibernate.cache.use_query_cache=true AND call setHint("org.hibernate.cacheable", true) on each query or criteria.Using L2 cache for tables frequently updated by external systems
Choosing the wrong CacheConcurrencyStrategy
Interview Questions on This Topic
Explain the 'N+1 Select Problem' and how the First Level Cache helps (or fails to help) in resolving it.
What is the difference between session.evict(entity) and session.clear() in Hibernate? When would you use one over the other in a production environment?
session.evict(entity) removes a single entity from the L1 cache (Persistence Context). session.clear() evicts all managed entities from the L1 cache. Use evict when you need to free memory for a specific large entity while keeping others in context. Use clear in batch processing to flush and then release all managed objects before the next batch, preventing OOM. In production, avoid excessive evict/clear as it breaks repeatable reads within the same transaction.Compare the four CacheConcurrencyStrategy types: READ_ONLY, NONSTRICT_READ_WRITE, READ_WRITE, and TRANSACTIONAL. Which is best for a high-concurrency e-commerce site?
How does the L2 cache handle entity relationships (One-to-Many, Many-to-One)? Does caching a parent entity automatically cache its children?
@Cacheable to be stored in L2 cache. If you cache a parent entity but not its child collection, loading the parent still triggers DB queries for the children. For collection caching, use @Cache(usage = ...) on the association field (e.g., @OneToMany). Be aware that cached collections are invalidated as a whole if any element changes, causing high churn for large collections.How do you ensure the L2 cache is invalidated when a bulk update query is executed? Explain the use of the @Modifying and @Query annotation with clearAutomatically = true in Spring Data JPA.
@Modifying(clearAutomatically = true) tells Spring Data JPA to automatically clear the L1 cache after a bulk update. However, it does NOT affect the L2 cache. To also invalidate L2, you must manually evict the affected region after the bulk operation. For example:
``
@Modifying(clearAutomatically = true)
@Query("UPDATE Product p SET p.price = :price WHERE p.id = :id")
int updatePrice(@Param("id") Long id, @Param("price") BigDecimal price);
// After calling this, also call: entityManager.getEntityManagerFactory().getCache().evict(Product.class);
``Frequently Asked Questions
No, the L1 cache is always enabled in Hibernate. It is part of the Persistence Context and cannot be disabled. However, you can clear it programmatically using or evict specific entities with session.clear()session.evict(entity).
Not by default. Standard L2 cache is local to the SessionFactory (per JVM). To share cache across instances, you need a distributed cache provider like Hazelcast, Infinispan, or Redis (via Redisson). Configure the region factory to use a clustered backend.
Hibernate will throw a HibernateException at commit time because it detects a mutation on a read-only entity. This is caught only during the flush phase, so you won't see it at , only at transaction commit.save()
No. The Query Cache stores only the identifiers (primary key values) of the entities that matched the query. When the cached query is reused, Hibernate fetches each entity by ID from the L2 entity cache or database. Therefore, Query Cache is only beneficial when Entity Cache is also enabled.
Use sessionFactory.getCache().evictEntityRegion(MyEntity.class) to evict an entire region, or evictAllRegions() for all regions. In Spring Boot, inject EntityManagerFactory and unwrap to SessionFactory to access caching methods.
20+ years shipping production Java in banking & fintech. Lessons pulled from things that broke in production.
That's Hibernate & JPA. Mark it forged?
7 min read · try the examples if you haven't