This page introduces the UserEventList, which is the common data type that will
be used in all implementations of our example application.

The goal is not only to introduce the specific data type used in our
examples, but to show some generic requirements for objects stored
in a cache:

  • Unmodifiable: Prevents accidental modification of a local copy
    in a distributed cache environment.
  • equals(Object) and hashCode(): equals(Object)
    is required by ConcurrentMap.replace(K, K, V).
    As shown in part 02, most cache implementations build on ConcurrentMap.
    is required because all Cache implementations use a hash-based map
    implementation internally.
  • Serializable: Required when objects are transferred over the network
    in clustered installations, or when objects are written to off-heap

The UserEventList used throughout the example application provides these
properties. Each instance represents the event list for a user.


New UserEventList instances can be created via static factory methods:

UserEventList list = UserEventList.emptyList();
UserEventList otherList = UserEventList.append(list, "A new event.");

The internal List<String> is copied for each new instance.

The total number of messages in a UserEventList is limited by SIZE_LIMIT,
i.e. when the SIZE_LIMIT + 1st String is appended, the new instance
will not contain the oldest entry.


public class UserEventList implements Serializable {

    private static final int SIZE_LIMIT = 10; // Max number of messages
    private final List<String> messages;

    public static UserEventList emptyList() {
        return new UserEventList(new ArrayList<String>());

    public static UserEventList append(UserEventList list, String msg) {
        List<String> newMessageList = new ArrayList<>();
        while ( newMessageList.size() > SIZE_LIMIT ) {
        return new UserEventList(newMessageList);

    private static String format(String msg) {
        DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        return dateFormat.format(new Date()) + " " + msg;

    private UserEventList(List<String> origMessageList) {
        if ( origMessageList == null || origMessageList.size() > SIZE_LIMIT ) {
            throw new IllegalArgumentException();
        this.messages = Collections.unmodifiableList(origMessageList);

    public List<String> getMessages() {
        return messages;

    public boolean equals(Object other) {
        if ( other instanceof UserEventList) {
            return messages.equals(((UserEventList) other).getMessages());
        return false;

    public int hashCode() {
        return messages.hashCode();


The final page in part 01 will show a ConcurrentMap-based implementation of
our example. We will refer to that implementation in the following parts
when we point out the differences between a cache and a ConcurrentMap: