Query Cache & Caching Strategies
Query Cache & Caching Strategies
The previous lesson introduced the second-level (L2) cache, which stores individual entity instances keyed by their primary key. The query cache goes one step further: it stores the result set of a named or criteria query — specifically, the ordered list of primary keys returned — and re-uses it on subsequent identical executions without hitting the database. Understanding when to enable it and which caching strategy to assign each entity is what separates a well-tuned data layer from one that silently wastes resources.
How the Query Cache Works
When you mark a query as cacheable and it executes for the first time, Hibernate stores a cache entry whose key is the query string plus the bound parameter values. The value stored is not the hydrated entity objects — it is just the list of primary keys (and for scalar queries, the raw column values). On the next call with the same query and parameters, Hibernate retrieves the key list from the query cache, then looks up each entity in the L2 entity cache (or loads it from the database if the entity is not cached there).
@Cacheable, Hibernate will hit the database for every entity on every query-cache hit — often worse than no caching at all, because you still pay the cache-lookup overhead.
Enable the query cache in application.properties:
Mark a JPQL query as cacheable via the @QueryHints annotation or the JPA hint constant:
From a Session or EntityManager you can also set the hint programmatically:
Cache Regions
Each entity class and each cacheable query is stored in a named cache region. The default region name for an entity is its fully qualified class name (e.g. com.example.shop.Product). The default region for query results is default-query-results-region. You can override the region on a query:
Named regions let you configure different TTL and eviction policies per data set in your JCache / Ehcache / Caffeine configuration. Fast-moving data (e.g. current stock level) gets a 30-second TTL; slow reference data (e.g. country codes) can live for an hour.
Choosing a Caching Strategy
Hibernate exposes four concurrency strategies for the L2 cache. Picking the right one is the single most important caching decision you make per entity.
READ_ONLY— the entity is never updated after it is first persisted (think: lookup tables, currency codes, product categories). Highest throughput, zero lock overhead. Any attempt to update throws an exception. Use this whenever you can.NONSTRICT_READ_WRITE— entity is occasionally updated but stale reads are acceptable for a brief window. Cache is invalidated after an update, but no lock is held during the invalidation. Suitable for low-concurrency writes on non-critical data.READ_WRITE— uses a soft lock to maintain read-committed semantics: the entry is locked during the write, other threads see the database value (or nothing) instead of the stale cached value. Safe for entities updated under moderate concurrency. Slight performance cost per write.TRANSACTIONAL— full transactional cache, integrated with JTA. Guarantees repeatable-read cache semantics across cluster nodes. Requires a JTA transaction manager and a JTA-capable cache provider. Rarely needed; adds significant complexity.
Apply the strategy with @Cache:
READ_ONLY for reference data, READ_WRITE for mutable entities. Only reach for NONSTRICT_READ_WRITE if profiling shows write contention on a READ_WRITE entity and your domain truly tolerates brief stale reads.
Collection Caching
Entity collections (e.g. @OneToMany sets) have their own cache region and must be annotated separately:
Without the second @Cache on the collection, Hibernate skips the collection when writing to the cache, causing an N+1 load on every cache hit for the parent entity.
Cache Invalidation
The query cache region is automatically invalidated when any entity of a type involved in the cached query is written. This means a high write rate on Product will constantly invalidate every cached query that touches Product, turning the cache into overhead. Monitor cache hit rates with Spring Boot Actuator or Micrometer before committing to the query cache in a write-heavy scenario.
To evict a specific region programmatically:
When Not to Use the Query Cache
The query cache is worth enabling only when:
- The same query with the same parameters is executed repeatedly.
- The underlying data changes rarely relative to the read frequency.
- The result set is reasonably small (thousands of rows, not millions).
Summary
The query cache stores query result key lists and is only effective when paired with a correctly configured entity L2 cache. Choose READ_ONLY for immutable reference data, READ_WRITE for safely mutable entities, and reserve TRANSACTIONAL for distributed JTA environments. Cache collections explicitly or face N+1 loads on cache hits. Use Actuator metrics to verify your hit rate before and after enabling caching, and remember that high-write tables make the query cache a liability rather than an asset.