Custom CacheKeys

Last updated 2 months ago

Learn how to write custom cache keys

LightbulbWhat you'll learn

  • Know how to write custom cache keys

Person reading a bookPrerequisites

  • Unified API

WristwatchTime matters

Reading time: 15 minutes

Person in front of a laptopShould I read this?

This guide is for Developers.

The Unified API provides access to a generic and powerful caching framework through the method CapConnection#getCache(). Usually it is sufficient to utilize this cache through the UAPI methods and through the content bean caching abstraction. But at times it is helpful to manage custom cache entries by the Cache#get(CacheKey) method. This article shows how to write a custom subclass of CacheKey for this purpose.

There are three methods that a custom CacheKey subclass must implement:

  • equals()

  • hashCode()

  • evaluate(Cache)

evaluate(Cache)

In the evaluate(Cache) method, you can perform any computation and eventually return the value to be cached. The computation should use only immutable properties of the cache key object as its input. It may access Unified API objects and process the results algorithmically. The computation may retrieve other values from the cache using other cache keys. These rules ensure that the cache can detect dependencies of the computation, making sure that the computation result is evicted from the cache when the system state changes.

If you access other mutable information sources like application state or persistent data stores during the computation, you would have to take care of tracking the required dependencies yourself, going beyond the scope of this article.

Be careful when storing content beans in the cache key fields. You may get incorrect results if you accidentally store data views of the content beans, because data view objects freeze some of their attributes. If necessary, use DataviewHelper#getOriginal(T) to remove the data view aspect of content beans.

It is good practice to implement evaluate() as a one-liner, following this pattern:

private class FooCacheKey extends CacheKey {
@Override
public Foo evaluate(Cache cache) {
return doGetFoo(args);
}
}

// outer class

public Foo getFoo() {
if (cache==null) {
LOG.warn("Using " + getClass().getName() + " without cache. Ok for testing, too expensive for production.");
return doGetFoo(args);
} else {
return cache.get(new FooCacheKey(args));
}
}

@VisibleForTesting
Foo doGetFoo(Object[] args) {
...

This has several advantages:

  • You can unittest your business logic.

  • You can reuse the class without a cache, as simple library code, if it is not performance critical. (E.g. in a utility cm tool, or within an outer cache context.)

  • Business logic is clearly separated from the technical aspect of caching. You could easily replace the CoreMedia cache with something different.

equals() and hashCode()

The class CacheKey explicitly replaces the existing methods equals() and hashCode() of Object by abstract methods, because the default semantics of objects identity is rarely optimal for cache keys. Your implementation should use all fields of your subclass when computing the hash code, and it should compare all fields for equality.

The implementations of equals() and hashCode() must be fast, because they are called frequently even if the result of the computation is cached. In particular, these methods must not make remote calls or access system resources.

The methods equals() and hashCode() must not call back into the cache. Because the Unified API relies heavily on the cache, access to API methods is restricted in equals() and hashCode(). You may call the equal() and hashCode() methods of Unified API objects in your implementation. It is not allowed to call other Unified API methods, because such methods might retrieve values from the cache.

It is good practice to include the hash code of the cache key class itself in the hash code of the key. Therefore, different computations based on the same parameters - possibly just a single content reference - do not cause hash code collisions.

If possible, make all fields of the key class final and provide value in the constructor to avoid cache keys that change their identity while being used.

You may also want to override the method cacheClass(Cache, T), which determines the key’s cache class, which in turn determines the capacity the cache allocates for keys of this class. Do not forget to configure a capacity for new cache classes by calling Cache.setCapacity(String, long).

You may use the cache class com.coremedia.cap.heap to participate in the main memory caching of the Unified API. In that case, override the method weight(Object, T, int) to return an estimate of the heap footprint of the computation result in bytes. Add four times the value of the last parameter to account for the size of internal cache data structures in this case.

The following example shows a simple cache key that removes vowels from content names, which is not particularly helpful, but hopefully instructive.

public final class MyCacheKey extends CacheKey<String> {
  private final Content content;

  public MyCacheKey(Content content) {
    this.content = content;
  }

  @Override
  public String evaluate(Cache cache) throws Exception {
    return content.getName().replaceAll("[aeiou]", "");
  }

  @Override
  public boolean equals(Object o) {
    if (this == o) {
      return true;
    }
    if (o == null || !getClass().equals(o.getClass())) {
      return false;
    }

    MyCacheKey that = (MyCacheKey) o;

    return content.equals(that.content);
  }

  @Override
  public int hashCode() {
    return 31 * content.hashCode() + getClass().hashCode();
  }

  @Override
  public String cacheClass(Cache cache, String value) {
    return "com.coremedia.cap.heap";
  }

  @Override
  public int weight(Object key, String value, int numDependents) {
    return 44 + 4 * numDependents;
  }
}

See the JavaDoc of the classes Cache and CacheKey for further details.

Copyright © 2025 CoreMedia GmbH, CoreMedia Corporation. All Rights Reserved.
Loading...