Backend Development 18 min read

Cache Eviction Strategies and Java Cache Implementations

This article explains various cache eviction strategies, compares heap, off‑heap, disk and distributed cache types, and provides concrete Java implementations using Guava Cache, EhCache 3.x and MapDB with code examples and usage patterns such as Cache‑Aside and Cache‑As‑SoR.

Architecture Digest
Architecture Digest
Architecture Digest
Cache Eviction Strategies and Java Cache Implementations

1. Cache Eviction Strategies

Cache can be reclaimed based on space (e.g., limit to 10 MB), capacity (maximum number of entries), time (TTL – time‑to‑live, TTI – time‑to‑idle), or Java object references (soft and weak references). Common algorithms include FIFO (first‑in‑first‑out), LRU (least‑recently‑used) and LFU (least‑frequently‑used). Soft references allow the JVM to reclaim objects when memory is low, while weak references are reclaimed as soon as they are not strongly reachable.

2. Java Cache Types

Four main cache storage locations are discussed:

Heap cache – stores objects in the JVM heap; fast but can increase GC pause time.

Off‑heap (outside‑heap) cache – stores data in native memory, reducing GC impact and allowing larger capacities.

Disk cache – persists data on disk, surviving JVM restarts.

Distributed cache – shared across multiple JVMs, solving single‑node capacity and consistency issues (e.g., Redis, EhCache clustered with Terracotta).

3. Java Cache Implementations

3.1 Guava Cache (heap only)

Guava provides a lightweight, high‑performance heap cache.

Cache<String, String> myCache =
    CacheBuilder.newBuilder()
        .concurrencyLevel(4)
        .expireAfterWrite(10, TimeUnit.SECONDS)
        .maximumSize(10000)
        .build();

Configuration options include maximumSize (capacity‑based eviction), expireAfterWrite (TTL), expireAfterAccess (TTI), weakKeys/weakValues, softValues, invalidate methods, concurrencyLevel, and recordStats for hit‑rate statistics.

3.2 EhCache 3.x (supports heap, off‑heap, disk, and distributed)

CacheManager cacheManager = CacheManagerBuilder.newCacheManagerBuilder().build(true);
CacheConfigurationBuilder<String, String> cacheConfig =
    CacheConfigurationBuilder.newCacheConfigurationBuilder(
        String.class, String.class,
        ResourcePoolsBuilder.newResourcePoolsBuilder()
            .heap(100, EntryUnit.ENTRIES))
        .withDispatcherConcurrency(4)
        .withExpiry(Expirations.timeToLiveExpiration(Duration.of(10, TimeUnit.SECONDS)));
Cache<String, String> myCache = cacheManager.createCache("myCache", cacheConfig);

EhCache supports capacity‑based, space‑based, and time‑based eviction, as well as explicit invalidate/remove operations. Concurrency is handled internally (default 16) and can be tuned via withDispatcherConcurrency.

3.3 MapDB 3.x (heap, off‑heap, and disk options)

HTreeMap myCache = DBMaker.heapDB()
    .concurrencyScale(16)
    .make()
    .hashMap("myCache")
    .expireMaxSize(10000)
    .expireAfterCreate(10, TimeUnit.SECONDS)
    .expireAfterUpdate(10, TimeUnit.SECONDS)
    .expireAfterGet(10, TimeUnit.SECONDS)
    .create();

MapDB offers similar eviction controls and can be configured for off‑heap or memory‑mapped disk storage.

4. Cache Usage Patterns

4.1 Cache‑Aside – application code explicitly reads/writes the cache and falls back to the system‑of‑record (SoR) when needed.

// Read example
value = myCache.getIfPresent(key);
if (value == null) {
    value = loadFromSoR(key);
    myCache.put(key, value);
}

// Write example
writeToSoR(key, value);
myCache.put(key, value);

4.2 Cache‑As‑SoR – the cache is treated as the primary data store; a CacheLoader (read‑through) or CacheWriter (write‑through/write‑behind) handles interaction with the underlying SoR.

Read‑through example (Guava):

LoadingCache<Integer, Result<Category>> cache =
    CacheBuilder.newBuilder()
        .softValues()
        .maximumSize(5000)
        .expireAfterWrite(2, TimeUnit.MINUTES)
        .build(new CacheLoader<Integer, Result<Category>>() {
            @Override
            public Result<Category> load(Integer id) throws Exception {
                return categoryService.get(id);
            }
        });

Write‑through example (EhCache):

CacheManager cm = CacheManagerBuilder.newCacheManagerBuilder().build(true);
Cache<String, String> cache = cm.createCache("myCache",
    CacheConfigurationBuilder.newCacheConfigurationBuilder(String.class, String.class,
        ResourcePoolsBuilder.newResourcePoolsBuilder().heap(100, MemoryUnit.MB))
        .withLoaderWriter(new DefaultCacheLoaderWriter<String, String>() {
            @Override
            public void write(String key, String value) throws Exception {
                // write to SoR
            }
            @Override
            public void delete(String key) throws Exception {
                // delete from SoR
            }
        }));

Write‑behind (asynchronous) can also be achieved with EhCache’s CacheWriter, allowing batch or delayed writes to the SoR.

4.3 Copy Patterns – Copy‑On‑Read and Copy‑On‑Write protect against unintended modifications when caches store mutable objects. EhCache provides support for these patterns via a Copier interface.

JavaCacheGuavaCache EvictionEhcacheCache PatternsMapDB
Architecture Digest
Written by

Architecture Digest

Focusing on Java backend development, covering application architecture from top-tier internet companies (high availability, high performance, high stability), big data, machine learning, Java architecture, and other popular fields.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.