Ehcache
Open-source caching library for Java. Supports from in-application cache to distributed cache. Integrates with Hibernate, Spring Boot, etc.
EhCache
EhCache is a high-performance Java caching library designed to improve application performance through efficient data caching. Offering tiered storage, JSR-107 (JCache) compliance, and clustering capabilities via Terracotta, it stands as the most widely-used Java caching solution with extensive enterprise implementation track record.
Overview
EhCache is a mature Java caching library with over 20 years of history, maintaining its significant position in the Java ecosystem as of 2024. EhCache 3 represents a complete architectural overhaul from previous versions, introducing Java 8+ support, JSR-107 compliance, multi-tier hierarchical storage, and distributed caching capabilities through Terracotta server integration. Particularly in enterprise environments, it serves as a trusted caching solution with proven integration with Hibernate and standard adoption in Spring Framework applications.
Features
Key Capabilities
- Tiered Storage: Efficient data management through four tiers: On-Heap, Off-Heap, Disk, and Clustered
- JSR-107 Compliance: Complete support for standard JCache API with extended functionality
- Distributed Caching: Clustering and scale-out capabilities through Terracotta server integration
- High Performance: Basic operations on On-Heap under 500ns with concurrent access optimization
- Rich Expiration Features: Flexible configuration of TTL, TTI, and custom expiration policies
- Transaction Support: XA transaction support providing ACID guarantees
Architecture Characteristics
EhCache's technical advantages:
- Hierarchical Data Management: Hot data placement in fast tiers with cold data in slower but larger capacity tiers
- Off-Heap Storage: Utilization of memory outside Java heap to avoid GC impact and enable large-capacity caching
- Asynchronous Event Processing: Efficient cache event handling through listeners
- Flexible Serialization: Performance optimization through custom serializers
Performance Characteristics
- High-speed access under 500ns for On-Heap operations
- Concurrent access optimization for multi-core CPU environments
- Efficient memory utilization through tiered storage
- Scale-out performance via Terracotta clustering
Pros and Cons
Advantages
- Mature Implementation: High stability through 20+ years of development and operational experience
- Enterprise Ready: Hibernate integration and Spring Framework standard support
- Comprehensive Features: Complete feature set including tiered storage, distributed caching, and transaction support
- JSR-107 Compliance: Portability and vendor lock-in avoidance through standard APIs
- Flexible Configuration: Diverse configuration options through programmatic and XML configuration
- Scalability: Terabyte-scale cache support through Terracotta clustering
Disadvantages
- Complex Configuration: High learning curve due to configuration complexity from extensive features
- Memory Usage: Higher memory consumption compared to other libraries like Caffeine
- Performance Disadvantage: Read/write performance gaps when compared to modern libraries (Caffeine, etc.)
- Legacy Feel: Gap between longstanding design philosophy and modern architectures
- Terracotta Dependency: Operational complexity of Terracotta server when using distributed caching
- Java 8+ Constraint: Required adaptation to newer Java versions
References
Code Examples
Adding Maven Dependencies
<!-- EhCache Core dependency -->
<dependency>
<groupId>org.ehcache</groupId>
<artifactId>ehcache</artifactId>
<version>3.10.8</version>
</dependency>
<!-- Terracotta clustering functionality -->
<dependency>
<groupId>org.ehcache</groupId>
<artifactId>ehcache-clustered</artifactId>
<version>3.10.8</version>
</dependency>
<!-- Transaction functionality -->
<dependency>
<groupId>org.ehcache</groupId>
<artifactId>ehcache-transactions</artifactId>
<version>3.10.8</version>
</dependency>
<!-- Spring Boot integration -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
Basic Cache Operations
import org.ehcache.Cache;
import org.ehcache.CacheManager;
import org.ehcache.config.builders.CacheConfigurationBuilder;
import org.ehcache.config.builders.CacheManagerBuilder;
import org.ehcache.config.builders.ResourcePoolsBuilder;
import org.ehcache.config.units.EntryUnit;
import org.ehcache.config.units.MemoryUnit;
public class EhCacheBasicExample {
public static void main(String[] args) {
// Create CacheManager
CacheManager cacheManager = CacheManagerBuilder.newCacheManagerBuilder()
.withCache("userCache",
CacheConfigurationBuilder.newCacheConfigurationBuilder(
String.class, String.class,
ResourcePoolsBuilder.heap(100)
.offheap(10, MemoryUnit.MB)
.disk(100, MemoryUnit.MB, true)
)
)
.build(true);
// Get cache instance
Cache<String, String> userCache = cacheManager.getCache("userCache", String.class, String.class);
// Add data
userCache.put("user:1", "John Doe");
userCache.put("user:2", "Jane Smith");
userCache.put("user:3", "Bob Wilson");
// Retrieve data
String user1 = userCache.get("user:1");
System.out.println("User 1: " + user1);
// Check existence
boolean hasUser2 = userCache.containsKey("user:2");
System.out.println("User 2 exists: " + hasUser2);
// Remove data
userCache.remove("user:3");
// Check cache size
System.out.println("Cache size: " +
java.util.stream.StreamSupport.stream(userCache.spliterator(), false).count());
// Close CacheManager
cacheManager.close();
}
}
Tiered Storage Configuration
import org.ehcache.PersistentCacheManager;
import org.ehcache.config.builders.CacheManagerBuilder;
import org.ehcache.config.builders.CacheConfigurationBuilder;
import org.ehcache.config.builders.ResourcePoolsBuilder;
import org.ehcache.config.units.EntryUnit;
import org.ehcache.config.units.MemoryUnit;
public class EhCacheTieredExample {
public static void main(String[] args) {
// Configure persistent directory
PersistentCacheManager persistentCacheManager = CacheManagerBuilder.newCacheManagerBuilder()
.with(CacheManagerBuilder.persistence(new File("cache-data")))
.withCache("productCache",
CacheConfigurationBuilder.newCacheConfigurationBuilder(
Long.class, Product.class,
ResourcePoolsBuilder.newResourcePoolsBuilder()
.heap(100, EntryUnit.ENTRIES) // Heap: 100 entries
.offheap(50, MemoryUnit.MB) // Off-heap: 50MB
.disk(500, MemoryUnit.MB, true) // Disk: 500MB (persistent)
)
)
.build(true);
Cache<Long, Product> productCache = persistentCacheManager.getCache("productCache", Long.class, Product.class);
// Add product data
for (long i = 1; i <= 1000; i++) {
Product product = new Product(i, "Product " + i, 29.99 + i);
productCache.put(i, product);
}
// Simulate data access patterns
for (int round = 0; round < 5; round++) {
System.out.println("=== Round " + (round + 1) + " ===");
// Access hot data
for (long i = 1; i <= 20; i++) {
long startTime = System.nanoTime();
Product product = productCache.get(i);
long endTime = System.nanoTime();
if (round == 0) {
System.out.println("Product " + i + " access time: " + (endTime - startTime) + " ns");
}
}
// Access cold data
for (long i = 500; i <= 520; i++) {
Product product = productCache.get(i);
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
break;
}
}
persistentCacheManager.close();
}
static class Product {
private final Long id;
private final String name;
private final Double price;
public Product(Long id, String name, Double price) {
this.id = id;
this.name = name;
this.price = price;
}
// getters, equals, hashCode, toString methods...
}
}
Read-Through/Write-Through with CacheLoaderWriter
import org.ehcache.spi.loaderwriter.CacheLoaderWriter;
import org.ehcache.config.builders.CacheConfigurationBuilder;
import org.ehcache.config.builders.CacheManagerBuilder;
import org.ehcache.config.builders.ResourcePoolsBuilder;
public class EhCacheLoaderWriterExample {
// Database simulation
private static final Map<String, User> DATABASE = new ConcurrentHashMap<>();
public static void main(String[] args) {
// Initialize database
DATABASE.put("user:1", new User("user:1", "John Doe", "[email protected]"));
DATABASE.put("user:2", new User("user:2", "Jane Smith", "[email protected]"));
// CacheLoaderWriter implementation
CacheLoaderWriter<String, User> loaderWriter = new CacheLoaderWriter<String, User>() {
@Override
public User load(String key) throws Exception {
System.out.println("Loading from database: " + key);
// Simulate database access
Thread.sleep(100);
return DATABASE.get(key);
}
@Override
public void write(String key, User value) throws Exception {
System.out.println("Writing to database: " + key);
// Simulate database write
Thread.sleep(50);
DATABASE.put(key, value);
}
@Override
public void delete(String key) throws Exception {
System.out.println("Deleting from database: " + key);
Thread.sleep(50);
DATABASE.remove(key);
}
};
// Create CacheManager
CacheManager cacheManager = CacheManagerBuilder.newCacheManagerBuilder()
.withCache("userCache",
CacheConfigurationBuilder.newCacheConfigurationBuilder(
String.class, User.class,
ResourcePoolsBuilder.heap(100))
.withLoaderWriter(loaderWriter)
)
.build(true);
Cache<String, User> userCache = cacheManager.getCache("userCache", String.class, User.class);
// Read-Through: Automatic loading of data not in cache
System.out.println("=== Read-Through Test ===");
User user1 = userCache.get("user:1"); // Load from database
System.out.println("First access: " + user1.getName());
User user1Again = userCache.get("user:1"); // Access from cache
System.out.println("Second access: " + user1Again.getName());
// Write-Through: Writing to both cache and database
System.out.println("=== Write-Through Test ===");
User newUser = new User("user:3", "Bob Wilson", "[email protected]");
userCache.put("user:3", newUser); // Write to both cache and DB
// Verify Write-Through
User user3 = userCache.get("user:3"); // Access from cache
System.out.println("New user from cache: " + user3.getName());
// Direct database access verification
User user3FromDB = DATABASE.get("user:3");
System.out.println("New user from database: " + user3FromDB.getName());
// Delete-Through: Removal from both cache and database
System.out.println("=== Delete-Through Test ===");
userCache.remove("user:2");
System.out.println("Remaining users in cache:");
userCache.forEach((key, value) ->
System.out.println(" " + key + ": " + value.getName()));
System.out.println("Remaining users in database:");
DATABASE.forEach((key, value) ->
System.out.println(" " + key + ": " + value.getName()));
cacheManager.close();
}
static class User {
private final String id;
private final String name;
private final String email;
public User(String id, String name, String email) {
this.id = id;
this.name = name;
this.email = email;
}
public String getId() { return id; }
public String getName() { return name; }
public String getEmail() { return email; }
}
}
Spring Boot Integration
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.stereotype.Service;
@SpringBootApplication
@EnableCaching
public class EhCacheSpringBootApplication {
public static void main(String[] args) {
SpringApplication.run(EhCacheSpringBootApplication.class, args);
}
}
@Service
public class UserService {
@Cacheable(value = "users", key = "#userId")
public User findUser(Long userId) {
// Simulate database access
System.out.println("Fetching user from database: " + userId);
simulateSlowDatabase();
return new User(userId, "User " + userId, "user" + userId + "@example.com");
}
@CachePut(value = "users", key = "#user.id")
public User updateUser(User user) {
System.out.println("Updating user: " + user.getId());
simulateSlowDatabase();
return user;
}
@CacheEvict(value = "users", key = "#userId")
public void deleteUser(Long userId) {
System.out.println("Deleting user: " + userId);
simulateSlowDatabase();
}
@CacheEvict(value = "users", allEntries = true)
public void clearUserCache() {
System.out.println("Clearing all user cache");
}
private void simulateSlowDatabase() {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
# application.yml - EhCache configuration
spring:
cache:
type: ehcache
ehcache:
config: classpath:ehcache.xml
# Cache names (for JSR-107 configuration)
# spring.cache.cache-names=users,products,orders
<!-- src/main/resources/ehcache.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://www.ehcache.org/v3"
xmlns:jsr107="http://www.ehcache.org/v3/jsr107"
xsi:schemaLocation="
http://www.ehcache.org/v3 https://www.ehcache.org/schema/ehcache-core-3.0.xsd
http://www.ehcache.org/v3/jsr107 https://www.ehcache.org/schema/ehcache-107-ext-3.0.xsd">
<cache alias="users">
<key-type>java.lang.Long</key-type>
<value-type>com.example.User</value-type>
<expiry>
<ttl unit="minutes">10</ttl>
</expiry>
<listeners>
<listener>
<class>com.example.CacheEventLogger</class>
<event-firing-mode>ASYNCHRONOUS</event-firing-mode>
<event-ordering-mode>UNORDERED</event-ordering-mode>
<events-to-fire-on>CREATED</events-to-fire-on>
<events-to-fire-on>UPDATED</events-to-fire-on>
<events-to-fire-on>EXPIRED</events-to-fire-on>
<events-to-fire-on>REMOVED</events-to-fire-on>
<events-to-fire-on>EVICTED</events-to-fire-on>
</listener>
</listeners>
<resources>
<heap unit="entries">1000</heap>
<offheap unit="MB">50</offheap>
</resources>
</cache>
<cache alias="products">
<key-type>java.lang.String</key-type>
<value-type>com.example.Product</value-type>
<expiry>
<tti unit="minutes">5</tti>
</expiry>
<resources>
<heap unit="entries">500</heap>
<offheap unit="MB">20</offheap>
</resources>
</cache>
<cache-template name="default">
<key-type>java.lang.Object</key-type>
<value-type>java.lang.Object</value-type>
<expiry>
<ttl unit="minutes">5</ttl>
</expiry>
<resources>
<heap unit="entries">100</heap>
</resources>
</cache-template>
</config>
Clustering Configuration (Terracotta)
import org.ehcache.clustered.client.config.builders.ClusteringServiceConfigurationBuilder;
import org.ehcache.clustered.client.config.builders.TimeoutsBuilder;
import org.ehcache.config.units.MemoryUnit;
public class EhCacheClusteredExample {
public static void main(String[] args) {
// Create clustered CacheManager
PersistentCacheManager cacheManager = CacheManagerBuilder.newCacheManagerBuilder()
.with(ClusteringServiceConfigurationBuilder.cluster(
URI.create("terracotta://localhost:9410/my-application"))
.autoCreate(server -> server
.defaultServerResource("primary-server-resource")
.resourcePool("resource-pool-a", 32, MemoryUnit.MB)
.resourcePool("resource-pool-b", 16, MemoryUnit.MB))
.timeouts(TimeoutsBuilder.timeouts()
.read(Duration.ofSeconds(10))
.write(Duration.ofSeconds(10))
.connection(Duration.ofSeconds(150)))
)
.withCache("clustered-cache",
CacheConfigurationBuilder.newCacheConfigurationBuilder(
String.class, String.class,
ResourcePoolsBuilder.newResourcePoolsBuilder()
.heap(100, EntryUnit.ENTRIES)
.with(ClusteredResourcePoolBuilder.clusteredDedicated("primary-server-resource", 8, MemoryUnit.MB))
)
)
.build(true);
Cache<String, String> clusteredCache = cacheManager.getCache("clustered-cache", String.class, String.class);
// Distributed cache operations
clusteredCache.put("shared-key-1", "Shared Value 1");
clusteredCache.put("shared-key-2", "Shared Value 2");
// Accessible from other application instances
String sharedValue = clusteredCache.get("shared-key-1");
System.out.println("Shared value: " + sharedValue);
cacheManager.close();
}
}
Statistics and Monitoring
import org.ehcache.config.builders.CacheConfigurationBuilder;
import org.ehcache.config.builders.CacheManagerBuilder;
import org.ehcache.config.builders.ResourcePoolsBuilder;
import org.ehcache.core.statistics.CacheStatistics;
import org.ehcache.core.statistics.DefaultStatisticsService;
public class EhCacheStatisticsExample {
public static void main(String[] args) throws InterruptedException {
// Enable statistics service
DefaultStatisticsService statisticsService = new DefaultStatisticsService();
CacheManager cacheManager = CacheManagerBuilder.newCacheManagerBuilder()
.using(statisticsService)
.withCache("monitoredCache",
CacheConfigurationBuilder.newCacheConfigurationBuilder(
String.class, String.class,
ResourcePoolsBuilder.heap(100)
)
)
.build(true);
Cache<String, String> monitoredCache = cacheManager.getCache("monitoredCache", String.class, String.class);
// Execute cache operations
for (int i = 0; i < 1000; i++) {
monitoredCache.put("key" + i, "value" + i);
}
// Measure hit ratio through random access
Random random = new Random();
for (int i = 0; i < 2000; i++) {
int keyIndex = random.nextInt(1500); // Expand key range to generate misses
String value = monitoredCache.get("key" + keyIndex);
}
// Get statistics
CacheStatistics statistics = statisticsService.getCacheStatistics("monitoredCache");
System.out.println("=== EhCache Statistics ===");
System.out.println("Cache hits: " + statistics.getCacheHits());
System.out.println("Cache misses: " + statistics.getCacheMisses());
System.out.println("Hit ratio: " + String.format("%.2f%%", statistics.getCacheHitPercentage()));
System.out.println("Cache puts: " + statistics.getCachePuts());
System.out.println("Cache removals: " + statistics.getCacheRemovals());
System.out.println("Cache evictions: " + statistics.getCacheEvictions());
// Periodic statistics monitoring
Thread monitoringThread = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
try {
Thread.sleep(5000);
System.out.println("--- Cache Monitor ---");
System.out.println("Hit ratio: " + String.format("%.2f%%", statistics.getCacheHitPercentage()));
System.out.println("Total operations: " + (statistics.getCacheHits() + statistics.getCacheMisses()));
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
});
monitoringThread.start();
Thread.sleep(30000); // Monitor for 30 seconds
monitoringThread.interrupt();
cacheManager.close();
}
}
Event Listener Implementation
import org.ehcache.event.CacheEvent;
import org.ehcache.event.CacheEventListener;
import org.ehcache.event.EventType;
import org.ehcache.config.builders.CacheEventListenerConfigurationBuilder;
public class EhCacheEventListenerExample {
public static void main(String[] args) {
// Custom event listener implementation
CacheEventListener<String, String> listener = new CacheEventListener<String, String>() {
@Override
public void onEvent(CacheEvent<? extends String, ? extends String> event) {
System.out.println("Cache Event: " + event.getType() +
" - Key: " + event.getKey() +
" - Old Value: " + event.getOldValue() +
" - New Value: " + event.getNewValue());
}
};
CacheManager cacheManager = CacheManagerBuilder.newCacheManagerBuilder()
.withCache("eventCache",
CacheConfigurationBuilder.newCacheConfigurationBuilder(
String.class, String.class,
ResourcePoolsBuilder.heap(50))
.withService(CacheEventListenerConfigurationBuilder
.newEventListenerConfiguration(listener, EventType.CREATED, EventType.UPDATED, EventType.REMOVED, EventType.EXPIRED, EventType.EVICTED)
.unordered().asynchronous()
)
)
.build(true);
Cache<String, String> eventCache = cacheManager.getCache("eventCache", String.class, String.class);
System.out.println("=== Event Listener Demo ===");
// Generate events
eventCache.put("event1", "First Value"); // CREATED event
eventCache.put("event1", "Updated Value"); // UPDATED event
eventCache.put("event2", "Second Value"); // CREATED event
eventCache.remove("event2"); // REMOVED event
// Generate eviction events with large data
for (int i = 0; i < 100; i++) {
eventCache.put("evict" + i, "Value " + i); // EVICTED events will occur
}
// Wait for asynchronous events to complete
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
cacheManager.close();
}
}
EhCache, with its rich features and long track record, serves as a reliable caching solution particularly for enterprise Java applications.