Cache API for Play Framework applications providing both synchronous and asynchronous cache operations
—
Support for multiple cache instances using named cache injection with Play's dependency injection framework. This allows applications to use different cache configurations for different purposes (e.g., session cache, data cache, temporary cache).
The @NamedCache annotation is used to inject specific named cache instances.
/**
* Qualifier annotation for named cache injection
*/
@Qualifier
@Retention(RetentionPolicy.RUNTIME)
public @interface NamedCache {
/**
* The name of the cache to inject
* @return cache name
*/
String value();
}Usage Examples:
import play.cache.AsyncCacheApi;
import play.cache.SyncCacheApi;
import play.cache.NamedCache;
import javax.inject.Inject;
import java.util.concurrent.CompletionStage;
public class UserService {
private final AsyncCacheApi sessionCache;
private final AsyncCacheApi dataCache;
private final SyncCacheApi tempCache;
@Inject
public UserService(
@NamedCache("session") AsyncCacheApi sessionCache,
@NamedCache("data") AsyncCacheApi dataCache,
@NamedCache("temp") SyncCacheApi tempCache
) {
this.sessionCache = sessionCache;
this.dataCache = dataCache;
this.tempCache = tempCache;
}
// Use session cache for user sessions (short-lived)
public CompletionStage<Done> cacheUserSession(String sessionId, UserSession session) {
return sessionCache.set("session:" + sessionId, session, 3600); // 1 hour
}
// Use data cache for user data (longer-lived)
public CompletionStage<Done> cacheUserData(String userId, User user) {
return dataCache.set("user:" + userId, user, 86400); // 24 hours
}
// Use temp cache for temporary calculations (synchronous)
public void cacheTempResult(String key, Object result) {
tempCache.set("temp:" + key, result, 300); // 5 minutes
}
}Implementation class for the @NamedCache annotation, used internally by the DI framework.
/**
* Implementation of NamedCache annotation for dependency injection
* See https://issues.scala-lang.org/browse/SI-8778 for why this is implemented in Java
*/
public class NamedCacheImpl implements NamedCache, Serializable {
/**
* Constructor for creating NamedCache instances
* @param value the cache name
*/
public NamedCacheImpl(String value);
/**
* Get the cache name
* @return the cache name
*/
public String value();
/**
* Hash code implementation following java.lang.Annotation specification
* @return hash code
*/
public int hashCode();
/**
* Equals implementation for annotation comparison
* @param o object to compare
* @return true if equal
*/
public boolean equals(Object o);
/**
* String representation of the annotation
* @return string representation
*/
public String toString();
/**
* Returns the annotation type
* @return NamedCache.class
*/
public Class<? extends Annotation> annotationType();
}Scala code can reference the Java annotation through a type alias.
// Type alias in play.api.cache package object
package play.api
/**
* Contains the Cache access API
*/
package object cache {
type NamedCache = play.cache.NamedCache
}Usage Examples:
import play.api.cache.{AsyncCacheApi, NamedCache}
import javax.inject.Inject
class ProductService @Inject()(
@NamedCache("products") productCache: AsyncCacheApi,
@NamedCache("categories") categoryCache: AsyncCacheApi,
defaultCache: AsyncCacheApi // Default unnamed cache
) {
import scala.concurrent.duration._
import scala.concurrent.ExecutionContext
// Use product-specific cache
def cacheProduct(product: Product)(implicit ec: ExecutionContext) = {
productCache.set(s"product:${product.id}", product, 2.hours)
}
// Use category-specific cache
def cacheCategory(category: Category)(implicit ec: ExecutionContext) = {
categoryCache.set(s"category:${category.id}", category, 6.hours)
}
// Use default cache for temporary data
def cacheTempData(key: String, data: Any)(implicit ec: ExecutionContext) = {
defaultCache.set(s"temp:$key", data, 30.minutes)
}
}Configure multiple named caches in application.conf:
# Default cache configuration
play.cache.defaultCache = "default"
play.cache.bindCaches = ["session", "data", "temp"]
# EhCache configuration for multiple caches
play.cache.createBoundCaches = true
# Cache-specific configurations
ehcache {
caches {
default {
maxEntriesLocalHeap = 1000
timeToLiveSeconds = 3600
}
session {
maxEntriesLocalHeap = 5000
timeToLiveSeconds = 1800 # 30 minutes
}
data {
maxEntriesLocalHeap = 10000
timeToLiveSeconds = 86400 # 24 hours
}
temp {
maxEntriesLocalHeap = 500
timeToLiveSeconds = 300 # 5 minutes
}
}
}Custom Guice module for advanced cache binding:
import com.google.inject.AbstractModule;
import com.google.inject.name.Names;
import play.cache.AsyncCacheApi;
import play.cache.NamedCacheImpl;
public class CacheModule extends AbstractModule {
@Override
protected void configure() {
// Bind named caches
bind(AsyncCacheApi.class)
.annotatedWith(new NamedCacheImpl("redis"))
.to(RedisAsyncCacheApi.class);
bind(AsyncCacheApi.class)
.annotatedWith(new NamedCacheImpl("memory"))
.to(MemoryAsyncCacheApi.class);
bind(AsyncCacheApi.class)
.annotatedWith(new NamedCacheImpl("distributed"))
.to(HazelcastAsyncCacheApi.class);
}
}Use multiple caches in a hierarchy for different performance characteristics:
public class DataService {
private final SyncCacheApi l1Cache; // Fast in-memory cache
private final AsyncCacheApi l2Cache; // Slower but larger cache
private final AsyncCacheApi backupCache; // Distributed/persistent cache
@Inject
public DataService(
@NamedCache("l1") SyncCacheApi l1Cache,
@NamedCache("l2") AsyncCacheApi l2Cache,
@NamedCache("backup") AsyncCacheApi backupCache
) {
this.l1Cache = l1Cache;
this.l2Cache = l2Cache;
this.backupCache = backupCache;
}
public CompletionStage<Optional<Data>> getData(String key) {
// Try L1 cache first (synchronous, fast)
Optional<Data> l1Result = l1Cache.getOptional(key);
if (l1Result.isPresent()) {
return CompletableFuture.completedFuture(l1Result);
}
// Try L2 cache (asynchronous, larger)
return l2Cache.getOptional(key)
.thenCompose(l2Result -> {
if (l2Result.isPresent()) {
// Store in L1 for next time
l1Cache.set(key, l2Result.get(), 300);
return CompletableFuture.completedFuture(l2Result);
}
// Try backup cache
return backupCache.getOptional(key)
.thenApply(backupResult -> {
if (backupResult.isPresent()) {
// Store in both L1 and L2
l1Cache.set(key, backupResult.get(), 300);
l2Cache.set(key, backupResult.get(), 3600);
}
return backupResult;
});
});
}
}Use different caches for different types of data:
import play.api.cache.{AsyncCacheApi, NamedCache}
import javax.inject.Inject
import scala.concurrent.duration._
class CacheService @Inject()(
@NamedCache("user-sessions") sessionCache: AsyncCacheApi,
@NamedCache("api-responses") apiCache: AsyncCacheApi,
@NamedCache("static-content") staticCache: AsyncCacheApi,
@NamedCache("analytics") analyticsCache: AsyncCacheApi
) {
// Short-lived session data
def cacheUserSession(sessionId: String, session: UserSession) = {
sessionCache.set(s"session:$sessionId", session, 30.minutes)
}
// Medium-lived API responses
def cacheApiResponse(endpoint: String, response: ApiResponse) = {
apiCache.set(s"api:$endpoint", response, 2.hours)
}
// Long-lived static content
def cacheStaticContent(path: String, content: StaticContent) = {
staticCache.set(s"static:$path", content, 24.hours)
}
// Analytics data with custom expiration
def cacheAnalytics(key: String, data: AnalyticsData) = {
analyticsCache.set(s"analytics:$key", data, Duration.Inf) // No expiration
}
}Coordinate invalidation across multiple named caches:
public class CacheManager {
private final AsyncCacheApi userCache;
private final AsyncCacheApi sessionCache;
private final AsyncCacheApi dataCache;
@Inject
public CacheManager(
@NamedCache("users") AsyncCacheApi userCache,
@NamedCache("sessions") AsyncCacheApi sessionCache,
@NamedCache("data") AsyncCacheApi dataCache
) {
this.userCache = userCache;
this.sessionCache = sessionCache;
this.dataCache = dataCache;
}
// Invalidate all data related to a user
public CompletionStage<Done> invalidateUser(String userId) {
CompletionStage<Done> userEviction = userCache.remove("user:" + userId);
CompletionStage<Done> sessionEviction = sessionCache.remove("session:" + userId);
CompletionStage<Done> dataEviction = dataCache.remove("userdata:" + userId);
return CompletableFuture.allOf(
userEviction.toCompletableFuture(),
sessionEviction.toCompletableFuture(),
dataEviction.toCompletableFuture()
).thenApply(v -> Done.getInstance());
}
// Clear all caches (admin operation)
public CompletionStage<Done> clearAllCaches() {
return CompletableFuture.allOf(
userCache.removeAll().toCompletableFuture(),
sessionCache.removeAll().toCompletableFuture(),
dataCache.removeAll().toCompletableFuture()
).thenApply(v -> Done.getInstance())
.exceptionally(throwable -> {
// Some caches might not support removeAll()
if (throwable.getCause() instanceof UnsupportedOperationException) {
// Log warning and continue
return Done.getInstance();
}
throw new RuntimeException(throwable);
});
}
}public class CacheTestModule extends AbstractModule {
@Override
protected void configure() {
// Use in-memory caches for testing
bind(AsyncCacheApi.class)
.annotatedWith(new NamedCacheImpl("test-cache"))
.to(DefaultAsyncCacheApi.class);
}
}public class UserServiceTest {
@Inject
@NamedCache("test-cache")
private AsyncCacheApi testCache;
@Inject
private UserService userService;
@Test
public void testCacheOperations() {
// Test with named cache
User user = new User("123", "John Doe");
CompletionStage<Done> setResult = testCache.set("user:123", user, 3600);
CompletionStage<Optional<User>> getResult = testCache.getOptional("user:123");
// Verify cache operations
assertThat(setResult.toCompletableFuture().join()).isEqualTo(Done.getInstance());
assertThat(getResult.toCompletableFuture().join()).contains(user);
}
}// Use descriptive, hierarchical names
@NamedCache("user-sessions") // User session data
@NamedCache("product-catalog") // Product information
@NamedCache("search-results") // Search result caching
@NamedCache("api-rate-limits") // Rate limiting data
@NamedCache("temp-calculations") // Temporary computation results// Use configuration-driven cache selection
class ConfigurableCacheService @Inject()(
@NamedCache("primary") primaryCache: AsyncCacheApi,
@NamedCache("fallback") fallbackCache: AsyncCacheApi,
config: Configuration
) {
private val useFailover = config.get[Boolean]("cache.enable-failover")
def getCache: AsyncCacheApi = {
if (useFailover) fallbackCache else primaryCache
}
}public class RobustCacheService {
private final AsyncCacheApi primaryCache;
private final AsyncCacheApi fallbackCache;
@Inject
public RobustCacheService(
@NamedCache("primary") AsyncCacheApi primaryCache,
@NamedCache("fallback") AsyncCacheApi fallbackCache
) {
this.primaryCache = primaryCache;
this.fallbackCache = fallbackCache;
}
public CompletionStage<Optional<Data>> getWithFallback(String key) {
return primaryCache.getOptional(key)
.exceptionally(throwable -> {
// Log primary cache failure
logger.warn("Primary cache failed, trying fallback", throwable);
return Optional.<Data>empty();
})
.thenCompose(result -> {
if (result.isPresent()) {
return CompletableFuture.completedFuture(result);
}
// Try fallback cache
return fallbackCache.getOptional(key);
});
}
}Install with Tessl CLI
npx tessl i tessl/maven-com-typesafe-play--play-cache-2-11