CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/maven-com-typesafe-play--play-cache-2-11

Cache API for Play Framework applications providing both synchronous and asynchronous cache operations

Pending
Overview
Eval results
Files

named-caches.mddocs/

Dependency Injection and Named Caches

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).

Capabilities

@NamedCache Annotation (Java)

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
    }
}

NamedCacheImpl Class (Java)

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 Type Alias

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)
  }
}

Configuration Examples

Application Configuration

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
    }
  }
}

Guice Module Configuration

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);
    }
}

Multi-Cache Patterns

Cache Hierarchies

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;
                    });
            });
    }
}

Cache Specialization

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
  }
}

Cache Invalidation Coordination

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);
        });
    }
}

Testing with Named Caches

Test Configuration

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);
    }
}

Test Examples

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);
    }
}

Best Practices

Cache Naming Conventions

// 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

Configuration Management

// 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
  }
}

Error Handling

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

docs

action-caching.md

core-cache-operations.md

index.md

named-caches.md

tile.json