Mastering Hibernate 6 L2 Caching with Ehcache 3: The Definitive Guide

Hibernate 6 introduced a major architectural shift. By moving to the Jakarta EE namespace and embracing the JCache (JSR-107) standard, it changed how we interact with caching providers. If you are using Hibernate 6, the legacy hibernate-ehcache dependency is dead. To achieve high-performance data access today, you need the modern JCache bridge.

The Problem: Database Bottlenecks in Hibernate 6

Even with the performance improvements in Hibernate 6’s new SQM (Semantic Query Model), database latency remains the primary bottleneck for scaling. Without a Second Level (L2) Cache, every time a new Session (EntityManager) is opened—even for the same user—Hibernate is forced to hit the database for data that hasn’t changed.

This results in redundant SQL SELECT statements, higher DB CPU usage, and increased costs in cloud environments where you pay for IOPS and database instance sizing.

The Solution: Hibernate 6 + Ehcache 3 (JSR-107)

The modern solution for Hibernate 6 is to use Ehcache 3 as a JCache provider. This allows Hibernate to offload entity and collection state to memory, sharing it across all sessions in the SessionFactory.

Prerequisites

  • Java 11+: Hibernate 6 requires a minimum of Java 11 (it is Hibernate 7 that raises the baseline to Java 17).
  • Jakarta Persistence 3.x: The modern jakarta.persistence namespace.

Step 1: Hibernate 6 Dependencies

In Hibernate 6, you must use the hibernate-jcache module. Crucially, your Ehcache dependency must include the jakarta classifier to avoid namespace conflicts.

<dependencies>
    <!-- Hibernate 6 Core -->
    <dependency>
        <groupId>org.hibernate.orm</groupId>
        <artifactId>hibernate-core</artifactId>
        <version>6.6.54.Final</version>
    </dependency>

    <!-- The JCache Bridge for Hibernate 6 -->
    <dependency>
        <groupId>org.hibernate.orm</groupId>
        <artifactId>hibernate-jcache</artifactId>
        <version>6.6.54.Final</version>
    </dependency>

    <!-- Ehcache 3 with Jakarta Support (MANDATORY classifier) -->
    <dependency>
        <groupId>org.ehcache</groupId>
        <artifactId>ehcache</artifactId>
        <classifier>jakarta</classifier>
        <version>3.10.8</version>
    </dependency>
</dependencies>

Step 2: Hibernate 6 Properties

Update your hibernate.properties or persistence.xml. Note that while the properties still often use javax.cache in their names for backward compatibility with the JSR-107 spec, they point to the Jakarta-compatible JCache factory in Hibernate 6.

# Enable L2 and Query Caching
hibernate.cache.use_second_level_cache=true
hibernate.cache.use_query_cache=true

# Set the Region Factory to JCache
hibernate.cache.region.factory_class=jcache

# Define the Ehcache Provider for Hibernate 6
hibernate.javax.cache.provider=org.ehcache.jsr107.EhcacheCachingProvider

# Path to your XML configuration
hibernate.javax.cache.uri=classpath:ehcache.xml

# Recommended: Enable statistics for monitoring hit/miss ratios
hibernate.generate_statistics=true

Step 3: Modern ehcache.xml Configuration

Ehcache 3 uses a structured XML format. For Hibernate 6, the cache alias must match the Fully Qualified Domain Name (FQDN) of the entity or the collection role.

<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">

    <!-- Template for common entity settings -->
    <cache-template name="entity-defaults">
        <expiry><ttl unit="seconds">3600</ttl></expiry>
        <heap unit="entries">1000</heap>
    </cache-template>

    <!-- Caching the Product Entity -->
    <cache alias="com.example.app.model.Product" uses-template="entity-defaults">
        <resources>
            <heap unit="entries">2000</heap>
            <!-- Off-heap prevents GC pauses on large datasets -->
            <offheap unit="MB">128</offheap> 
        </resources>
    </cache>

    <!-- Caching a Collection (Product's Reviews) -->
    <cache alias="com.example.app.model.Product.reviews">
        <heap unit="entries">500</heap>
    </cache>
</config>

Step 4: Annotating Entities and Collections

Hibernate 6 requires standard JPA @Cacheable combined with Hibernate’s @Cache for concurrency strategy.

import jakarta.persistence.*;
import org.hibernate.annotations.Cache;
import org.hibernate.annotations.CacheConcurrencyStrategy;

@Entity
@Cacheable
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
public class Product {
    @Id @GeneratedValue 
    private Long id;
    
    private String name;

    @OneToMany(mappedBy = "product")
    @Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)
    private List<Review> reviews;
}

Concurrency Strategies Explained

  • READ_ONLY: Best for data that never changes. Fastest performance.
  • READ_WRITE: Uses “soft locks” to ensure consistency during updates. The safest default for most data.
  • NONSTRICT_READ_WRITE: Faster than READ_WRITE but allows potential stale reads if multiple transactions update the same record simultaneously.

Hibernate 6 Cache Management

In Hibernate 6, you can programmatically evict data through the Cache interface via the SessionFactory:

// Evict specific instance
sessionFactory.getCache().evictEntityData(Product.class, productId);

// Evict all products
sessionFactory.getCache().evictEntityData(Product.class);

// Evict a query region
sessionFactory.getCache().evictQueryRegion("slow-search-query");

Why This is Better in Hibernate 6

  1. Off-Heap Support: By moving cache data out of the JVM heap into direct RAM, you eliminate large Garbage Collection pauses.
  2. Standardization: By using JCache, you can swap Ehcache for Redis or Hazelcast without changing your Hibernate logic.
  3. Disassembled State: Hibernate 6 stores data in a “hydrated” format—a simple array of values. This uses significantly less memory than storing full Java objects.

Summary of Major Changes

  • Namespace: All persistence imports must be jakarta.persistence.
  • Bridge: hibernate-ehcache is replaced by hibernate-jcache.
  • Dependencies: Ehcache must use the jakarta classifier.
  • Configuration: Properties now reference jcache and EhcacheCachingProvider.

Frequently Asked Questions (FAQ)

1. What is the difference between READ_ONLY and READ_WRITE cache strategies? READ_ONLY is faster but should only be used for data that never changes. READ_WRITE includes locking mechanisms to ensure data consistency during updates.

2. How do I clear the Hibernate L2 cache manually? You can use the Cache API from the SessionFactory: sessionFactory.getCache().evictEntityData(Product.class);

Q 3: Should I use the same ehcache.xml for Hibernate 6 and Hibernate 7?

Mostly yes — the Ehcache 3 XML format is the same for both versions. The key difference is the Hibernate-level cache region names and configuration properties. In Hibernate 6, the entity cache alias should use the FQDN of the entity class (e.g., com.example.Product). In Hibernate 7, you can also use the region attribute of @Cache to specify a custom region name that matches your Ehcache alias, giving you cleaner separation.

Q 4: How do I migrate from the legacy hibernate-ehcache to hibernate-jcache?

The migration has four steps: (1) Remove the hibernate-ehcache dependency and add hibernate-jcache plus ehcache:jakarta. (2) Update the region factory property from org.hibernate.cache.ehcache.EhCacheRegionFactory to jcache. (3) Add hibernate.javax.cache.provider=org.ehcache.jsr107.EhcacheCachingProvider. (4) Convert any legacy ehcache.xml using the Ehcache 2.x format to the modern Ehcache 3 XML schema. The @Cache and @Cacheable annotations on entities remain unchanged.

Conclusion

Transitioning to Hibernate 6 requires a modern approach to caching. By abandoning the legacy proprietary providers and adopting the Ehcache 3 JCache implementation, you align your application with the latest Jakarta EE standards while unlocking significant performance gains.

The integration of L2 caching is no longer just an “extra” feature; for high-scale enterprise applications, it is a necessity to prevent database exhaustion. By following the steps outlined in this guide—from correctly configuring your pom.xml with the jakarta classifier to fine-tuning your ehcache.xml with off-heap storage—you can ensure your Java applications remain responsive, scalable, and cost-efficient.

Start by monitoring your cache hit ratios using Hibernate statistics and iteratively tune your TTL and heap sizes to find the “sweet spot” for your specific workload. Happy coding!

Further Reading & Cross-References

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.