Cache API for Play Framework applications providing both synchronous and asynchronous cache operations
—
Action-level HTTP caching provides built-in support for caching Play Framework action results with proper HTTP headers (ETag, Expires) for client-side caching coordination. This includes both server-side response caching and client cache validation.
The Cached class provides a fluent API for creating cacheable actions with various caching strategies.
/**
* A helper to add caching to an Action
*/
class Cached @Inject() (cache: AsyncCacheApi)(implicit materializer: Materializer) {
/**
* Cache an action with custom key and caching function
* @param key Compute a key from the request header
* @param caching Compute a cache duration from the resource header
* @return CachedBuilder for further configuration
*/
def apply(key: RequestHeader => String, caching: PartialFunction[ResponseHeader, Duration]): CachedBuilder
/**
* Cache an action with custom key (no expiration)
* @param key Compute a key from the request header
* @return CachedBuilder for further configuration
*/
def apply(key: RequestHeader => String): CachedBuilder
/**
* Cache an action with fixed key (no expiration)
* @param key Cache key
* @return CachedBuilder for further configuration
*/
def apply(key: String): CachedBuilder
/**
* Cache an action with custom key and duration in seconds
* @param key Compute a key from the request header
* @param duration Cache duration in seconds
* @return CachedBuilder for further configuration
*/
def apply(key: RequestHeader => String, duration: Int): CachedBuilder
/**
* Cache an action with custom key and duration
* @param key Compute a key from the request header
* @param duration Cache duration
* @return CachedBuilder for further configuration
*/
def apply(key: RequestHeader => String, duration: Duration): CachedBuilder
/**
* A cached instance caching nothing (useful for composition)
* @param key Compute a key from the request header
* @return CachedBuilder for further configuration
*/
def empty(key: RequestHeader => String): CachedBuilder
/**
* Caches everything, forever
* @param key Compute a key from the request header
* @return CachedBuilder for further configuration
*/
def everything(key: RequestHeader => String): CachedBuilder
/**
* Caches everything for the specified seconds
* @param key Compute a key from the request header
* @param duration Cache duration in seconds
* @return CachedBuilder for further configuration
*/
def everything(key: RequestHeader => String, duration: Int): CachedBuilder
/**
* Caches everything for the specified duration
* @param key Compute a key from the request header
* @param duration Cache duration
* @return CachedBuilder for further configuration
*/
def everything(key: RequestHeader => String, duration: Duration): CachedBuilder
/**
* Caches the specified status, for the specified number of seconds
* @param key Compute a key from the request header
* @param status HTTP status code to cache
* @param duration Cache duration in seconds
* @return CachedBuilder for further configuration
*/
def status(key: RequestHeader => String, status: Int, duration: Int): CachedBuilder
/**
* Caches the specified status, for the specified duration
* @param key Compute a key from the request header
* @param status HTTP status code to cache
* @param duration Cache duration
* @return CachedBuilder for further configuration
*/
def status(key: RequestHeader => String, status: Int, duration: Duration): CachedBuilder
/**
* Caches the specified status forever
* @param key Compute a key from the request header
* @param status HTTP status code to cache
* @return CachedBuilder for further configuration
*/
def status(key: RequestHeader => String, status: Int): CachedBuilder
/**
* A cached instance caching nothing (useful for composition)
* @param key Compute a key from the request header
* @return CachedBuilder for further configuration
*/
def empty(key: RequestHeader => String): CachedBuilder
/**
* Caches everything, forever
* @param key Compute a key from the request header
* @return CachedBuilder for further configuration
*/
def everything(key: RequestHeader => String): CachedBuilder
/**
* Caches everything for the specified seconds
* @param key Compute a key from the request header
* @param duration Cache duration in seconds
* @return CachedBuilder for further configuration
*/
def everything(key: RequestHeader => String, duration: Int): CachedBuilder
/**
* Caches everything for the specified duration
* @param key Compute a key from the request header
* @param duration Cache duration
* @return CachedBuilder for further configuration
*/
def everything(key: RequestHeader => String, duration: Duration): CachedBuilder
}Usage Examples:
import play.api.cache.Cached
import play.api.mvc._
import javax.inject.Inject
import scala.concurrent.duration._
class ProductController @Inject()(
cc: ControllerComponents,
cached: Cached
) extends AbstractController(cc) {
// Cache with request-based key
def getProduct(id: String) = cached(req => s"product:$id", 1.hour) {
Action { implicit request =>
val product = loadProduct(id)
Ok(Json.toJson(product))
}
}
// Cache everything for 30 minutes
def listProducts = cached.everything(req => "products", 30.minutes) {
Action { implicit request =>
val products = loadAllProducts()
Ok(Json.toJson(products))
}
}
// Cache only successful responses (200 status)
def getProductDetails(id: String) = cached.status(req => s"product-details:$id", 200, 1.hour) {
Action { implicit request =>
loadProduct(id) match {
case Some(product) => Ok(Json.toJson(product))
case None => NotFound("Product not found")
}
}
}
// Conditional caching based on response headers
def getProductWithConditionalCache(id: String) = cached(
key = req => s"product-conditional:$id",
caching = {
case header if header.status == 200 => 1.hour
case header if header.status == 404 => 5.minutes
}
) {
Action { implicit request =>
loadProduct(id) match {
case Some(product) => Ok(Json.toJson(product))
case None => NotFound("Product not found")
}
}
}
}The CachedBuilder provides additional configuration options for cached actions.
/**
* Builds an action with caching behavior
* Uses both server and client caches:
* - Adds an Expires header to the response, so clients can cache response content
* - Adds an Etag header to the response, so clients can cache response content and ask the server for freshness
* - Cache the result on the server, so the underlying action is not computed at each call
*/
final class CachedBuilder(
cache: AsyncCacheApi,
key: RequestHeader => String,
caching: PartialFunction[ResponseHeader, Duration]
)(implicit materializer: Materializer) {
/**
* Compose the cache with an action
* @param action The action to cache
* @return Cached EssentialAction
*/
def apply(action: EssentialAction): EssentialAction
/**
* Compose the cache with an action
* @param action The action to cache
* @return Cached EssentialAction
*/
def build(action: EssentialAction): EssentialAction
/**
* Whether this cache should cache the specified response if the status code match
* This method will cache the result forever
* @param status HTTP status code to cache
* @return New CachedBuilder with status-based caching
*/
def includeStatus(status: Int): CachedBuilder
/**
* Whether this cache should cache the specified response if the status code match
* This method will cache the result for duration seconds
* @param status HTTP status code to cache
* @param duration Cache duration in seconds
* @return New CachedBuilder with status-based caching
*/
def includeStatus(status: Int, duration: Int): CachedBuilder
/**
* Whether this cache should cache the specified response if the status code match
* This method will cache the result for duration
* @param status HTTP status code to cache
* @param duration Cache duration
* @return New CachedBuilder with status-based caching
*/
def includeStatus(status: Int, duration: Duration): CachedBuilder
/**
* The returned cache will store all responses whatever they may contain
* @param duration Cache duration
* @return New CachedBuilder with default caching
*/
def default(duration: Duration): CachedBuilder
/**
* The returned cache will store all responses whatever they may contain
* @param duration Cache duration in seconds
* @return New CachedBuilder with default caching
*/
def default(duration: Int): CachedBuilder
/**
* Whether this cache should cache the specified response if the status code match
* This method will cache the result forever
* @param status HTTP status code to cache
* @return New CachedBuilder with status-based caching
*/
def includeStatus(status: Int): CachedBuilder
/**
* Compose the cache with new caching function
* @param alternative A closure getting the response header and returning the duration we should cache for
* @return New CachedBuilder with composed caching logic
*/
def compose(alternative: PartialFunction[ResponseHeader, Duration]): CachedBuilder
}Usage Examples:
import play.api.cache.Cached
import play.api.mvc._
import javax.inject.Inject
import scala.concurrent.duration._
class ApiController @Inject()(
cc: ControllerComponents,
cached: Cached
) extends AbstractController(cc) {
// Complex caching configuration with builder methods
def getApiData(category: String) = cached
.empty(req => s"api:$category")
.includeStatus(200, 1.hour) // Cache successful responses for 1 hour
.includeStatus(404, 5.minutes) // Cache not found responses for 5 minutes
.default(30.seconds) { // Default cache for other responses
Action { implicit request =>
loadApiData(category) match {
case Some(data) => Ok(Json.toJson(data))
case None => NotFound("Category not found")
}
}
}
// Composing multiple caching strategies
def getDataWithComposition(id: String) = cached
.status(req => s"data:$id", 200, 2.hours)
.compose {
case header if header.headers.contains("X-Cache-Control") => 10.minutes
case header if header.status == 500 => Duration.Zero // Don't cache errors
} {
Action { implicit request =>
// Action implementation
Ok("data")
}
}
}The @Cached annotation provides a simple way to cache action results in Java controllers.
/**
* Mark an action to be cached on server side
*/
@With(CachedAction.class)
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Cached {
/**
* The cache key to store the result in
* @return the cache key
*/
String key();
/**
* The duration the action should be cached for. Defaults to 0.
* @return the duration in seconds (0 = no expiration)
*/
int duration() default 0;
}The CachedAction class implements the caching behavior for @Cached annotated methods.
/**
* Cache another action
*/
public class CachedAction extends Action<Cached> {
@Inject
public CachedAction(AsyncCacheApi cacheApi);
/**
* Execute the cached action
* @param req HTTP request
* @return CompletionStage containing the cached or computed result
*/
public CompletionStage<Result> call(Request req);
}Usage Examples:
import play.cache.Cached;
import play.mvc.*;
import javax.inject.Inject;
import java.util.concurrent.CompletionStage;
public class ProductController extends Controller {
@Inject
private ProductService productService;
// Cache with fixed key and 1-hour expiration
@Cached(key = "products.all", duration = 3600)
public CompletionStage<Result> listProducts() {
return productService.getAllProducts()
.thenApply(products -> ok(Json.toJson(products)));
}
// Cache with dynamic key based on request parameter
@Cached(key = "product.{id}", duration = 1800) // 30 minutes
public CompletionStage<Result> getProduct(String id) {
return productService.getProduct(id)
.thenApply(product -> {
if (product.isPresent()) {
return ok(Json.toJson(product.get()));
} else {
return notFound("Product not found");
}
});
}
// Cache without expiration (permanent until manually evicted)
@Cached(key = "product.categories")
public CompletionStage<Result> getCategories() {
return productService.getCategories()
.thenApply(categories -> ok(Json.toJson(categories)));
}
// Cache at class level - applies to all methods
@Cached(key = "admin.stats", duration = 300) // 5 minutes
public static class AdminController extends Controller {
public CompletionStage<Result> getStats() {
return CompletableFuture.supplyAsync(() ->
ok(Json.toJson(generateStats())));
}
}
}The caching system automatically generates ETags and handles client cache validation:
// Automatic ETag generation based on content and expiration
// Client sends: If-None-Match: "etag-value"
// Server responds: 304 Not Modified if content unchangedCached responses automatically include appropriate HTTP headers:
HTTP/1.1 200 OK
ETag: "a1b2c3d4e5f6"
Expires: Wed, 09 Sep 2025 21:30:00 GMT
Cache-Control: public, max-age=3600Internal class for serializing Play Results for caching:
/**
* Wraps a Result to make it Serializable
* Only strict entities can be cached, streamed entities cannot be cached
*/
private[play] final class SerializableResult(constructorResult: Result) extends Externalizable {
def this()
def result: Result
override def readExternal(in: ObjectInput): Unit
override def writeExternal(out: ObjectOutput): Unit
}
private[play] object SerializableResult {
val encodingVersion: Byte = 2
}// Static key
cached("homepage")
// Request-based key
cached(req => s"user:${req.session.get("userId").getOrElse("anonymous")}")
// Path-based key
cached(req => s"api:${req.path}")
// Parameter-based key
cached(req => s"product:${req.getQueryString("id").getOrElse("all")}")import scala.concurrent.duration._
// Fixed duration
cached(key, 1.hour)
// Conditional duration based on response
cached(key, {
case header if header.status == 200 => 1.hour
case header if header.status == 404 => 5.minutes
case _ => Duration.Zero // Don't cache other responses
})
// Status-specific caching
cached.status(key, 200, 2.hours) // Only cache 200 responsescached.empty(key)
.includeStatus(200, 1.hour) // Cache successful responses
.includeStatus(404, 5.minutes) // Cache not found briefly
.compose { // Add conditional logic
case header if header.headers.contains("X-No-Cache") => Duration.Zero
case _ => 30.seconds // Default fallback
}Some cache implementations may not support removeAll():
cache.removeAll().recover {
case _: UnsupportedOperationException =>
// Handle unsupported clear operation
Done
}Only strict HTTP entities can be cached:
// This will work - strict entity
Ok("Hello World")
// This will fail - streamed entity
Ok.chunked(source) // Throws IllegalStateException during caching// Handle cache misses gracefully
cached.getOrElseUpdate("key", 1.hour) {
computeExpensiveValue().recover {
case ex: Exception =>
logger.error("Failed to compute value", ex)
defaultValue
}
}Install with Tessl CLI
npx tessl i tessl/maven-com-typesafe-play--play-cache-2-11