Creating a simple and generic cache manager in Java

Marcello Passos
6 min readMay 3, 2020

--

In several applications, the use of cache strategies represents a great performance gain. Scenarios where we have frequent data reading, rare writing and little variation in queries can be brutally optimized with the wise use of this feature.

Currently, there are great cache implementations available to developers with varying levels of robustness. We could briefly mention JCache and EhCache as examples. It is not the intention of this article to present a “competitor” or a complete alternative for these solutions. Before that, the main goal is, in a practical way, to implement a cache management timeout based, stored in memory, extremely simple and generic enough to be useful in multiple scenarios. The logic used is similar to the concept of “item expire timeout” presented by Oracle:

“An [item expire timeout] specify how long items can stay in the item [cache] before they are invalidated. For example, if the item-expire-timeout for a cached item is set to 60000 (milliseconds), its data becomes stale after 60 seconds; and the item must be reloaded from the database when it is next accessed.” (Oracle ATG Repository Guide — Cache Timeout)

In the end, we gonna see how to integrate our cache manager in a specific case.

Get to work!

First, let’s define the interface of our cache manager. I mean: what operations will it provide? In harmony with the KISS principle, the operations defined are:

  • Put: stores a new entry to the cache, associated with a search key.
  • Get: given a key, retrieves a value stored in the cache.
  • Remove: given a key, removes an entry from the cache.
  • Contains key: check if a certain key is already stored in the cache.
  • Clean: removes all expired cache entries.
  • Clear: removes all entries from the cache.
package br.com.marcellogpassos.genericcache;

import java.util.Optional;

public interface IGenericCache<K, V> {

void clean();

void clear();

boolean containsKey(K key);

Optional<V> get(K key);

void put(K key, V value);

void remove(K key);
}

Rigtht now, we can see that our cache is a kind of data dictionary (or a Map) where the types of the search key and stored value are generic. Then, let’s implement this interface:

package br.com.marcellogpassos.genericcache;

import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;

public class GenericCache<K, V> implements IGenericCache<K, V> {

public static final Long DEFAULT_CACHE_TIMEOUT = 60000L;

protected Map<K, CacheValue<V>> cacheMap;
protected Long cacheTimeout;

public GenericCache() {
this(DEFAULT_CACHE_TIMEOUT);
}

public GenericCache(Long cacheTimeout) {
this.cacheTimeout = cacheTimeout;
this.clear();
}

@Override
public void clean() {
for(K key: this.getExpiredKeys()) {
this.remove(key);
}
}

@Override
public boolean containsKey(K key) {
return this.cacheMap.containsKey(key);
}

protected Set<K> getExpiredKeys() {
return this.cacheMap.keySet().parallelStream()
.filter(this::isExpired)
.collect(Collectors.toSet());
}

protected boolean isExpired(K key) {
LocalDateTime expirationDateTime = this.cacheMap.get(key).getCreatedAt().plus(this.cacheTimeout, ChronoUnit.MILLIS);
return LocalDateTime.now().isAfter(expirationDateTime);
}

@Override
public void clear() {
this.cacheMap = new HashMap<>();
}

@Override
public Optional<V> get(K key) {
this.clean();
return Optional.ofNullable(this.cacheMap.get(key)).map(CacheValue::getValue);
}

@Override
public void put(K key, V value) {
this.cacheMap.put(key, this.createCacheValue(value));
}

protected CacheValue<V> createCacheValue(V value) {
LocalDateTime now = LocalDateTime.now();
return new CacheValue<V>() {
@Override
public V getValue() {
return value;
}

@Override
public LocalDateTime getCreatedAt() {
return now;
}
};
}

@Override
public void remove(K key) {
this.cacheMap.remove(key);
}

protected interface CacheValue<V> {
V getValue();

LocalDateTime getCreatedAt();
}

}

As we can see, the timeout for the items kept in the cache can assume the default value defined in the static variable:

public static final Long DEFAULT_CACHE_TIMEOUT = 60000L;

Or can be passed as a parameter in the construction of the class, as well. The data structure that stores the cache contents in memory is defined in:

protected Map<K, CacheValue<V>> cacheMap;

Actually, this variable is nothing more than a HashMap of search keys of type K for cache values that are of type… CacheValue. But what the hell is this CacheValue about? The answer is in the very same class:

protected interface CacheValue<V> {
V getValue();

LocalDateTime getCreatedAt();
}

CacheValue is an internal interface that probably has no semantic value outside our own GenericCache class. This interface only stores the value of the “cached” item (which is of the generic type V) and its creation date/time (java.time.LocalDateTime). Based on this creation date/time and the cacheTimeout we can calculate whether an entry in the cache is expired or not. You may have noticed that our CacheValue interface could pretty much be replaced by some implementation of Pair<V, LocalDateTime> in java, such as Apache Commons or JavaFX. However, I believe that the cost of adding another dependency to the project just for that is not worth it.

Therefore, caching consists of placing a record in the cacheMap with the CacheValue created:

@Override
public void put(K key, V value) {
this.cacheMap.put(key, this.createCacheValue(value));
}

protected CacheValue<V> createCacheValue(V value) {
LocalDateTime now = LocalDateTime.now();
return new CacheValue<V>() {
@Override
public V getValue() {
return value;
}

@Override
public LocalDateTime getCreatedAt() {
return now;
}
};
}

The “cleaning” of our cache manager is performed on demand whenever someone tries to get some value from the cache. Before returning the desired value, all expired entries are automatically removed by the clean method:

@Override
public void clean() {
for (K key : this.getExpiredKeys()) {
this.remove(key);
}
}

@Override
public boolean containsKey(K key) {
return this.cacheMap.containsKey(key);
}

protected Set<K> getExpiredKeys() {
return this.cacheMap.keySet().parallelStream()
.filter(this::isExpired)
.collect(Collectors.toSet());
}

protected boolean isExpired(K key) {
LocalDateTime expirationDateTime = this.cacheMap.get(key).getCreatedAt().plus(this.cacheTimeout, ChronoUnit.MILLIS);
return LocalDateTime.now().isAfter(expirationDateTime);
}

How to use?

So let’s imagine the following usage scenario for this cache manager:

We are developing an application with the SpringBoot framework that analyzes the data of Github users. In this application we have a service class called GithubUsersService, responsible for abstracting the business rules related to the data of Github users. This class also retrieves data for a specific Github user, through its rest API. Without using the cache and using only a rest repository implementation such as OpenFeign, for example, the class would look something like this:

package br.com.marcellogpassos.githubanalyzer;

import org.springframework.stereotype.Service;

@Service
public class GithubUsersService {

private final GithubUsersRestRepository repository;

public GithubUsersService(GithubUsersRestRepository repository) {
this.repository = repository;
}

public GithubUser getSingleUser(String username) {
return this.repository.getSingleUser(username).orElseThrow(UsernameNotFoundException::new);
}
}

As we know, http requests are costly operations for application performance. Thankfully, the client reported that a delay of up to 5 minutes in updating the data of the users we consulted through the API rest is absolutely tolerable. May we agree that it is not common for users to update their data on Github several times a day. Why, then, not use our GenericCache? As a result, we would have:

package br.com.marcellogpassos.githubanalyzer;

import org.springframework.stereotype.Service;

@Service
public class GithubUsersService {

private final GithubUsersRestRepository repository;

private final GenericCache<String, GithubUser> cache;

public GithubUsersService(GithubUsersRestRepository repository, GenericCache<String, GithubUser> cache) {
this.repository = repository;
this.cache = cache;
}

public GithubUser getSingleUser(String username) {
return this.cache.get(username).orElseGet(() -> this.fromRepository(username));
}

public GithubUser fromRepository(String username) {
GithubUser user = this.repository.getSingleUser(username).orElseThrow(UsernameNotFoundException::new);
this.cache.put(username, user);
return user;
}
}

As you can see in the getSingleUser method, we first tried to retrieve the user from the cache. Only when it is not possible do we access the rest repository. Once the user is retrieved from the repository, we take advantage of the return and add it to the cache. From then on, all calls to the getSingleUser method will no longer need to carry out the expensive http request until the cache value expires.

However, a point is not yet clear: where and how is this IGenericCache<String, GithubUser> instantiated? Obviously, respecting the principles of inversion of control and dependency injection, this occurs outside the GithubUsersService class. SpringBoot makes this task easier for us:

import br.com.marcellogpassos.genericcache.GenericCache;
import br.com.marcellogpassos.genericcache.IGenericCache;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class CacheConfig {

@Bean
public <K, V> IGenericCache<K, V> getCache(@Value("${app.cache-timeout}") Long cacheTimeout) {
return new GenericCache<K, V>(cacheTimeout);
}
}

That’s it! the getCache method is able to instantiate an IGenericCache as a dependency for any application class. Not just the IGenericCache<String, GithubUser>. Any cache with any type of key and any type of value! The cache timeout is also injected by Spring itself. In our case it is being defined in the application properties:

app.cache-timeout = 300000

From there the possibilities are endless. We can even create two different caches for the same type of key/value using different names for the beans and the Qualifier annotation.

I hope you enjoyed. See you later!

--

--