How to implement a generic in memory cache in Java

I would like to show you how to create a pretty simple in memory cache in Java that can hold a single object. This may help you to take some load off your database for example.

Java caching options

Back in 2001 the Java Specification Request 107 proposed the idea of a caching API. It took almost exactly 13 years and in 2014 the final specification was released. During this time a lot of different solutions evolved and as a developer you have a wide range of options and can pick the one that fits best. Some of them are:

  • Ehcache
  • Google Guava -- Cache
  • Java Caching System (JCS)
  • Cache2k
  • Caffeine
  • Spring Caching

Or even data structures that help you to build a caching system like redis.

Single object in memory cache

Since you can roll your own caching class in less than 50 lines of code it may be sensible to choose this as a very simple and lightweight alternative. Have a look at the following:

public class SingleObjectCache<T> implements Serializable {

	private static final long serialVersionUID = 1L;

	private T obj;
	private long storageTime;
	private final long ttl;

	public SingleObjectCache(final long ttl) {
		this.ttl = ttl;
	}

	public synchronized Optional<T> get() {
		if (this.obj == null)
			return Optional.empty();

		final long now = Instant.now().toEpochMilli();
		if (now - this.storageTime > this.ttl) {
			this.obj = null;
			this.storageTime = 0;
			return Optional.empty();
		}

		return Optional.of(this.obj);
	}

	public synchronized void set(final T obj) {
		this.obj = obj;
		this.storageTime = Instant.now().toEpochMilli();
	}

	public synchronized Optional<T> getAndSet(final Supplier<T> objSupplier) {
		final Optional<T> obj = this.get();
		if (obj.isPresent())
			return obj;

		this.set(objSupplier.get());

		return this.get();
	}
}

Using generics the cache can be parameterized with the type of object you want to place in your cache. You call the constructor with a time to live in milliseconds and after this time the object is invalidated and has to be refreshed. You can then place an object in the cache with the set method and retrieve it with the get method. The get method takes care of resetting the object in case it got too old.

It even has a getAndSet convenience method that helps you to get the object from the cache if it exists and in case it is not present it is set with the given supplier and then returned. The cache uses java.util.Optional instead of throwing around null references.

Testing the cache

A unit test for the cache with a User class can look like so:

class User {
  // ...
}

@Test
public void test() {
  final SingleObjectCache<User> cache = new SingleObjectCache<>(1000 * 10);

  // -> false
  assertFalse(cache.get().isPresent());

  // -> true
  cache.set(new User("testuser"));
  assertTrue(cache.get().isPresent());
}

@Test
public void testGetAndSet() {
  final SingleObjectCache<User> cache = new SingleObjectCache<>(1000 * 10);

  // -> false
  assertFalse(cache.get().isPresent());

  // -> true
  assertTrue(cache.getAndSet(() -> new User("testuser")).isPresent());
}

We initialize the cache and the first call to the get() method does not retrieve a result since we haven’t put anything in the cache yet. After placing an object in the cache we can get it. The getAndSet method is just a convenient way to bundle the retrieval of the object for the cache and the actual get for the cached object in one place which makes sense if you need this simple cache at one location in your code only; if you get and set the objects at different locations in your code it’s not so useful.

More of a real world example

Let’s say you wrote a web application and your session instance holds a reference to the username. When checking certain authorization constraints you retrieve the user object from the database using this username. Instead of fetching the user object from the database with every method call you can either place a cache in the database layer or in the view layer in the session class.

Let’s assume you want to put the cache in the view layer for now. But first let’s have a look at the overall code.

/*
 * Database layer
 */
class UserDO {
	private UserRoleDO userRole;

	// More user properties here...

	public UserRoleDO getUserRole() {
		return userRole;
	}

	public void setUserRole(UserRoleDO userRole) {
		this.userRole = userRole;
	}
}

enum UserRoleDO {
	ADMINISTRATOR,
	AUTHOR;
}

class UserService {
	public UserDO getByUsername(final String username) {
		// Simulate long running database request
		try {
			Thread.sleep(200);
		} catch (InterruptedException ex) {
		}

		// Actual database call here instead...
		final UserDO user = new UserDO();
		user.setUserRole(UserRoleDO.ADMINISTRATOR);
		return user;
	}
}

We have a UserDO class and a UserRoleDO enum in the database layer of the application. Then we have a UserService class that helps us to fetch a certain user by its username. Whether this call takes 20 or 200 milliseconds to complete does not really matter -- we are just trying to simulate here that it takes a certain amount of time and if you would call the method ofter it would compound to a considerable amount of time that is spent fetching the user object from the database. If that wasn’t the case we wouldn’t consider adding a cache in the first place, right?

/*
 * Web framework / View layer
 */
enum Role {
	ADMIN,
	USER
};

class MyWebSession {

	private String username;
	private UserService userService = new UserService();

	public List<Role> getRoles() {
		final Optional<UserDO> user = this.getUser();
		if (user.isPresent() == false)
			return new ArrayList<>();

		switch (user.get().getUserRole()) {
		case ADMINISTRATOR:
			return Arrays.asList(Role.ADMIN, Role.USER);
		case AUTHOR:
			return Arrays.asList(Role.USER);
		default:
			throw new IllegalStateException();
		}
	}

	public Optional<UserDO> getUser() {
		if (StringUtils.isBlank(this.username))
			return Optional.empty();

		return Optional.ofNullable(this.userService.getByUsername(this.username));
	}
}

The view layer has a WebSession class and a separate Role enum that is used for authorization checking. The different components that make up your web page that you want to render when a certain URL is requested will call the getRoles() method on the session instance. This in turn will fetch the user object from the database using the user service.

public void testRun() {
	final MyWebSession myWebSession = new MyWebSession();
	myWebSession.username = "testuser";
	for (int i = 0; i < 10; i++)
		myWebSession.getRoles();
}

Finally we have a small testRun() method that simulates this behavior of multiple components in your view layer calling the getRoles() method on the session instance.

Now let’s add the cache to the view layer and more specifically to the session class in this example. As you can see I just added a new private property to the class that holds our cache. The getUser() method will lazy-initialize the cache even though this is not really necessary in this case because the cache instance is super lightweight. Then instead of calling the user service to fetch the user object from the database all the time we ask the cache to get the cached user object instance. As I said earlier the getAndSet() method helps us to get an object from the cache and declare the way how it is set in the same place.

class MyWebSession {

  private static final long USER_OBJECT_CACHE_TTL = 1000 * 120; // 2min
  private SingleObjectCache<UserDO> userObjCache;

  public Optional<UserDO> getUser() {
    if (StringUtils.isBlank(this.username))
      return Optional.empty();

    // Lazy-initialization
    if (Objects.isNull(this.userObjCache))
      this.userObjCache = new SingleObjectCache<>(USER_OBJECT_CACHE_TTL);

    // Instead of this:
    // return Optional.ofNullable(this.userService.getByUsername(this.username));
    // We call the cache:
    return Optional.ofNullable(
      this.userObjCache.getAndSet(
        () -> this.userService.getByUsername(this.username)).get() //
    );
  }

// ...

After these few code changes let’s have a look at a unit test. We just call the simulation method testRun() on both classes: The one that is using no cache and the one that has the cache in the session class. The version that has the cache fetches the user object just once from the database -- which takes 200 milliseconds in this example -- and then has almost no overhead retrieving the cached object.

Closing thoughts

As you can see setting up a cache can be pretty easy and it may help you to significantly speed up certain operations in your application. Before adding a cache to your code please keep in mind to analyze potential performance bottlenecks first. Maybe it’s possible to rearrange your code in a way that solves the performance issue without even adding a cache.