1. Overview

In this tutorial, we'll introduce caching to an existing service using the proxy pattern. So the service calls will return the cached results if that method is called before.

2. Sample Application

Let's first look at our sample application.

public interface UserService {

    List<String> getUsers(String country);

    int getAccessCount();
}

We have the UserService interface which returns the users for a country. Note that we're also defining getAccessCount to test whether caching is successful.

public class UserServiceImpl implements UserService {

    private final Map<String, List<String>> users = ImmutableMap.of("us", Lists.newArrayList("user1", "user2"),
                                                                    "en", Lists.newArrayList("user3", "user4", "user5"));
    private int count;

    @Override
    public List<String> getUsers(String country) {
        count++;
        return users.get(country);
    }

    @Override
    public int getAccessCount() {
        return count;
    }
}

UserServiceImpl is the default implementation.

3. Caching

Next, we'll use the proxy pattern to introduce caching.

The resulting proxy will be a caching proxy that is wrapping the default implementation. Moreover, the proxy class will contain its own data structure to cache the results.

public class CachingUserServiceProxy implements UserService {

    private final UserService userService;

    private final ConcurrentMap<String, List<String>> cache;

    private final Object writeLock = new Object();

    public CachingUserServiceProxy(UserService userService) {
        this.userService = userService;
        this.cache = new ConcurrentHashMap<>();
    }

    @Override
    public List<String> getUsers(String country) {
        if (!cache.containsKey(country)) {
            synchronized (writeLock) {
                if (!cache.containsKey(country)) {
                    List<String> users = userService.getUsers(country);
                    cache.put(country, users);
                }
            }
        }

        return cache.get(country);
    }

    @Override
    public int getAccessCount() {
        return userService.getAccessCount();
    }
}

Here, CachingUserServiceProxy stores the cache results in a ConcurrentMap. To satisfy the concurrency requirements, we're also using double-checked locking.

Now let's look at a sample client code:

public class ClientMain {

    public static void main(String[] args) {
        UserService cachingProxy = new CachingUserServiceProxy(new UserServiceImpl());
        cachingProxy.getUsers("us");
        cachingProxy.getUsers("us");
        cachingProxy.getUsers("en");
        cachingProxy.getUsers("en");
        System.out.println("Access count: " + cachingProxy.getAccessCount());
    }
}

When we run the client, it prints the access count:

Access count: 2

Although we've called getUsers four times, the access count is two. So we can verify that caching was successful.

4. Summary

In this quick tutorial, we've introduced caching to an existing class using the proxy pattern.

As always, the source code for all examples is available on Github.