Reduce Repeated Database Reads with Hibernate Second-Level Cache and Ehcache
How Hibernate's second-level cache with Ehcache serves repeated entity reads from memory, plus consistency and heap trade-offs.
19 Jun 2026, 17:14 UTC

Your database is answering the same read over and over
An endpoint loads a User, then renders user.getOrders(). Hibernate issues one SELECT for the row and another for the child collection. The next request repeats both queries. If that profile is popular, your database returns identical rows thousands of times per hour while the application waits.
Hibernate's first-level cache lives inside a single session and disappears when the session closes. The second-level cache is scoped to the SessionFactory, so it survives across sessions and transactions. With Ehcache as the provider, frequently read entity data can be served from JVM memory instead of the database.
This is not a drop-in speedup. You must choose a concurrency strategy, size the cache, and understand what happens when data changes outside Hibernate.
Configure the cache for a read-mostly entity
The following configuration is representative for Hibernate 6.x with Ehcache 3.x through JCache. Property names differ in Hibernate 5.x, so check your version's documentation before copying.
Add the Ehcache JCache dependency to your build, then enable the second-level cache in application.properties:
hibernate.cache.use_second_level_cache=true
hibernate.cache.region.factory_class=org.hibernate.cache.jcache.JCacheRegionFactory
hibernate.javax.cache.provider=org.ehcache.jsr107.EhcacheCachingProvider
hibernate.javax.cache.uri=classpath:ehcache.xml
Mark the entity cacheable. READ_WRITE is appropriate when the same application instance updates the data and you need committed reads; NONSTRICT_READ_WRITE has less overhead but occasional stale reads are possible.
@Entity
@Cacheable
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
@Table(name = "app_user")
public class User {
@Id
private Long id;
private String email;
// ...
}
Define the region in src/main/resources/ehcache.xml. This example caps the region at 10,000 entries and expires entries after five minutes:
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://www.ehcache.org/v3">
<cache alias="com.example.app.User">
<expiry><ttl unit="seconds">300</expiry>
<resources><heap unit="entries">10000</resources>
</cache>
</config>
Run this change in a non-production branch first. Required permissions are ordinary read/write access to the repository and classpath resources; no database privileges are needed to enable the cache itself.
Measure before you assume it works
Enable Hibernate statistics locally and compare the JDBC query count for repeated reads of the same identifiers:
Statistics statistics = sessionFactory.getStatistics();
statistics.setStatisticsEnabled(true);
long before = statistics.getQueryExecutionCount();
// Load the same 100 user IDs again, for example in a loop or repeated requests.
long after = statistics.getQueryExecutionCount();
If the cache is effective, repeated reads of cached identifiers should not increase queryExecutionCount at the same rate as uncached reads. Check Ehcache hit/miss ratios through JMX or the Ehcache management console; a rising hit count confirms entities are being served from the region.
Do not treat a lower query count as proof by itself. Confirm that the same logical data is returned, and watch heap usage after the cache warms up.
Trade-offs: heap pressure and stale data
- Memory pressure. Each cached entity consumes heap.
READ_WRITElocks and copies entries, which adds overhead. Set explicit heap limits and eviction policies in Ehcache to avoidOutOfMemoryError. - Stale data. If another service updates the same row through direct SQL or a different persistence unit, Hibernate does not automatically evict the cached entity. Use short time-to-live values, programmatic eviction, or cache invalidation when external writes occur.
- Wrong workload. The second-level cache helps read-mostly data. It adds little value for write-heavy tables, and it does not replace
JOIN FETCHfor fixing N+1 collection loading.
Actionable closing
Pick one read-mostly entity, enable the second-level cache with a bounded Ehcache region, turn on statistics, and measure queryExecutionCount and cache hit ratio under realistic load. If hit ratio is high and heap stays flat, expand to similar entities. If data is updated outside Hibernate, add explicit eviction before you roll out more widely.
To undo the change, remove the cache properties, annotations, and Ehcache configuration, then redeploy. No database migration is required because the cache sits outside the database.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.