Ehcache

Open-source caching library for Java. Supports from in-application cache to distributed cache. Integrates with Hibernate, Spring Boot, etc.

cache-serverJavaTerracottaclustered-cacheoff-heapJSR-107JCache

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.