Cache functionality on Google Guava

Google Guava has a precise support to implement minor caching needs of any Java application. Guava’s implementation supports caching on the local memory and thus it should be cautiously used keeping in mind a few limitations.

This cannot be used for requirements where the cache grows to a very high size as it might impact the application performance due to an overloaded memory consumption. This also does not have network based shared cache and thus cannot support if you require any clustered caching. If the application needs a simple caching implementation which does not grow to a high size and required only for quicker access without persisting / network sharing requirements, Guava is an easy choice due to its simplicity of implementation.

Guava supports two types of cache implementation based on how the data is loaded into the cache –

LoadingCache – where a Loader is provided upfront and as and when a request arrives to obtain some entry from cache, the appropriate value is computed, added into cache and served back as response. Further calls would be served faster as the value for a given key is already present in the cache. All required values can be computed upfront by invoking a call to getAll method which accepts an Iterable as the input.

@Test
public void testLoadingCache() throws ExecutionException {
    LoadingCache cache = CacheBuilder.newBuilder().build(new CacheLoader() {
        @Override
        public String load(String key) throws Exception {
            sleep(1000); // dummy delay
            return cacheMap.get(key);
        }
    });

    Stopwatch stopwatch = Stopwatch.createStarted();
    assertEquals("one", cache.get("1"));
    stopwatch.stop();
    assertTrue(stopwatch.elapsed(MILLISECONDS) > 1000);

    stopwatch.reset();
    stopwatch.start();
    assertEquals("one", cache.get("1"));
    stopwatch.stop();
    assertTrue(stopwatch.elapsed(MILLISECONDS) < 100);
}

Cache – where the value for a given key is explicitly added. Initialization returns an empty Cache object which can be used to store the key-value pairs that would be accessed in the future.

@Test
public void testCache() throws ExecutionException {
    Cache cache = CacheBuilder.newBuilder().build();
    cache.putAll(cacheMap);

    Stopwatch stopwatch = Stopwatch.createStarted();
    assertEquals("one", cache.getIfPresent("1"));
    stopwatch.stop();
    assertTrue(stopwatch.elapsed(MILLISECONDS) < 100); stopwatch.reset(); stopwatch.start(); assertEquals("four", cache.get("4", () -> "four"));// add to cache and get
    stopwatch.stop();
    long t1 = stopwatch.elapsed(MILLISECONDS);

    stopwatch.reset();
    stopwatch.start();
    assertEquals("four", cache.get("4", () -> "four"));// already in cache, just get
    stopwatch.stop();
    long t2 = stopwatch.elapsed(MILLISECONDS);
    assertTrue(t1 > t2);
}

Any existing entry can be retrieved using getIfPresent method with the key as input. Both the implementations support a get method which accepts a key and a Callable implementation which can be invoked if the key does not exist in the cache.

Cache entries can be evicted explicitly by invoking invalidate method or also by setting few policies for automatic eviction based on cache size or time duration after write /access. Note that the eviction based on policies does not happen at the exact expiry of the key, but it happens along with simultaneous read or write operations. This ensures a dedicated thread is not consumed just to housekeep the cache entries, if required, an explicit cleanUp method can be called to clear expired entries.

// time based
Cache<String, String> cache = CacheBuilder.newBuilder().expireAfterWrite(1, SECONDS).build();
cache = CacheBuilder.newBuilder().expireAfterAccess(1, SECONDS).build();

// max size
cache = CacheBuilder.newBuilder().maximumSize(5).build();

//invalidation
cache.invalidate("1");

Caching framework also supports registering Removal Listeners and stats for advanced usage to perform any post processing or to run analytics.