Learn through the super-clean Baeldung Pro experience:
>> Membership and Baeldung Pro.
No ads, dark-mode and 6 months free of IntelliJ Idea Ultimate to start with.
Last updated: March 18, 2024
In this tutorial, we’ll learn how to use Play’s caching API with Scala. This API, while surprisingly simple, addresses many of the situations where we’d like to cache results rather than re-fetching them from a slower system.
To use the caching APIs in our Play application, there is some set-up to be done at the project level.
First, we need to amend our project’s build.sbt with an additional dependency:
libraryDependencies += caffeine
Play also supports using Ehcache, but they recommend Caffeine as of this writing.
Having added that dependency to our project, we’ll need to restart sbt or reload the project in our IDE. Once we’ve done that, we should be ready to start using the caching APIs in our code.
Play provides two different types of caching: synchronous and asynchronous. For our purposes, we’ll focus on the async version since it’s a better fit for a web service.
The AsyncCacheApi supports several operations:
Note that the synchronous version of the API, SyncCacheApi, does not provide the removeAll() method.
Next, we’ll see how to use the getOrElseUpdate() method.
By way of example, we’ll use the caching API for a simple application that uses Twitter’s recent search API. This API returns tweets from the last seven days for a specified user.
Using caching here makes sense for a couple of reasons:
In our sample code, we’ll use the Twitter user name as the key into our cache. We’ll also set the entries to expire after five minutes. This is configurable via the twitterCache.expiry setting in the application.conf file.
Our top-level class is an instance of a Play BaseController called TwitterController:
@Singleton
class TwitterController @Inject()(
twitterSearchService: TwitterSearchService,
override val controllerComponents: ControllerComponents,
implicit val executionContext: ExecutionContext
) extends BaseController {
def recentSearch(twitterAccount: String): Action[AnyContent] = Action.async {
twitterSearchService.recentSearch(twitterAccount).map { response =>
Ok(Json.toJson(response))
}
}
}
We’ll make our controller available to an external caller by configuring it in the Play routes file:
GET /api/twitter/recentSearch/:twitterAccount controllers.TwitterController.recentSearch(twitterAccount)
Once we’ve launched our Play application, we can interact with the above interface using curl:
curl http://localhost:9000/api/twitter/recentSearch/TwitterDev | jq
Next, let’s look at how we’ve implemented the service layer, as this is where we use caching.
Our service layer code is also very simple, and this is where we’ll use the caching API provided by Play:
class TwitterSearchService @Inject()(twitterWebApi: TwitterWebApi,
cache: AsyncCacheApi,
configuration: Configuration,
implicit val executionContext: ExecutionContext) {
val cacheExpiry: Duration = configuration.get[Duration]("twitterCache.expiry")
def recentSearch(twitterUser: String): Future[Map[String, JsValue]] = {
cache.getOrElseUpdate[JsValue](twitterUser, cacheExpiry) {
twitterWebApi.recentSearch(twitterUser)
}.map(_.as[Map[String, JsValue]])
}
}
As discussed above, the getOrElseUpdate() method retrieves the specified tweets from the cache if they exist, otherwise, it calls our wrapper for the Twitter API to retrieve the latest values. This is a very common usage pattern for any cache. This one API call will probably suffice for the great majority of applications.
The client code to fetch tweets uses the Play WSClient class and does no caching of its own:
def recentSearch(fromTwitterUser: String): Future[JsValue] = {
val url = String.format(recentSearchUrl, fromTwitterUser)
wsClient
.url(url)
.withHttpHeaders(
HeaderNames.ACCEPT -> MimeTypes.JSON,
HeaderNames.AUTHORIZATION -> s"Bearer $bearerToken"
).get()
.map { response =>
if (response.status == OK) {
response.json
} else {
throw ApiError(response.status, Some(response.statusText))
}
}
}
Note that the Twitter API requires us to use a bearer token to authenticate. This will be issued once we’ve created a Twitter developer account. Also, note that we have to set the header directly because the Play WSRequest.withAuth() method only works for authentication methods that have a username and password such as Basic Auth.
It’s worth noting that neither the SyncCacheApi nor the AsyncCacheApi provides any inter-thread synchronization. This means that for a given key, the API we have used could result in multiple calls to Twitter.
Additionally, the Caffeine provider we’ve used here is not a distributed cache. Therefore, if we run multiple instances of our Play app, each will have its own cache. It is possible to configure Ehcache to provide this.
In this tutorial, we introduced and discussed using Play’s caching API. As we can see, Play makes it very easy to add this capability to our application.