I’ve been on a caching crusade. I’m working to reduce the load on our database by focusing on various caching solutions. In an ideal world, data that is “static” will be heavily cached; minimal database interaction required. However, that’s not the world I’m living in. Each request to our GraphQL API leads directly to a database hit. Ugh!
Apollo GraphQL offers a few caching solutions to help with this endeavor. We decided to go with the responseCachePlugin. This plugin stores cacheable data in a datastore of your choice, for us that was Redis. The key thing to keep in mind is that the data must be cacheable. Which leads us to “the problem.”
The problem
The queries that the clients were structuring contained a mix of public and private data. Here’s an example. The below query for getSimilarEvents
requests three fields that are public, thus cacheable: id
, title
, and imageUrl
. However, it also requests data that is private to the specific user, and thus not cacheable: shortUrl
and isSaved
.
query getSimilarEvents($eventId: String!) {
similarEvents(filter: {eventId: $eventId}, input: {first: 10}) {
edges {
node {
id,
title,
imageUrl,
shortUrl,
isSaved
}
}
}
}
When the responseCachePlugin
encounters this type of query, the default is to return the value of the least cacheable field. For private data, that means no caching. Definitely not what we want. So what can we do to fix this?
The solution
We need to split the GraphQL query into two requests. The first request will query for the highly, cacheable public data. The second request will retrieve the private data. Let’s see how Kotlin flows and the reduce function make light work of this task.
Separating the GraphQL query
The first thing we need to do is create two new queries. These queries will take in the same input and retrieve the same number of records. The key difference is that now, we are only requesting the appropriate public or private fields.
Additionally, it’s critical that both queries include the id
field so that we can combine the objects later.
query getSimilarEventsPublic($eventId: String!) { similarEvents(filter: {eventId: $eventId}, input: {first: 10}) { edges { node { id, title, imageUrl } } } } query getSimilarEventsPrivate($eventId: String!) { similarEvents(filter: {eventId: $eventId}, input: {first: 10}) { edges { node { id, shortUrl, isSaved } } } }
With the two new queries created we can make the needed API requests and combine the data.
Combining Kotlin flows
The first step to combining the data is creating a new data class to hold our two separate lists of events.
data class SimilarEventsResponse( val eventsPublic: List<GetSimilarEventsPublicQuery.Edge>, val eventsPrivate: List<GetSimilarEventsPrivateQuery.Edge> )
Next, using the Apollo Kotlin client library, we’ll create two functions one for each query. Both functions return a flow.
private fun getSimilarEventsNearbyPublic( eventId: String, ): Flow<List<GetSimilarEventsPublicQuery.Edge>> { return apolloClient.query( GetSimilarEventsPublicQuery(eventId = eventId) ) .toFlow() .map { it.data?.similarEvents?.edges } } private fun getSimilarEventsNearbyPrivate( eventId: String, ): Flow<List<GetSimilarEventsPrivateQuery.Edge>> { return apolloClient.query( GetSimilarEventsPrivateQuery(eventId = eventId) ) .toFlow() .map { it.data?.similarEvents?.edges } }
Finally, we’ll use the flow combine function to combine the most recently emitted values from each flow.
fun getSimilarEvents(eventId: String): Flow<SimilarEventsResponse> { return combine( getSimilarEventsNearbyPublic(eventId), getSimilarEventsNearbyPrivate(eventId), ) { eventsPublic, eventsPrivate -> SimilarEventsResponse(eventsPublic, eventsPrivate) } }
Merging the results
We’ve done it. We have two separate lists of events. One list contains the public data and the other contains the private data. Let’s merge them together. To do that, we’ll take the following steps:
- Grab each list from our data class
- Add the lists together
- Use the groupingBy function to create a grouping of the events based on
id
- Use the reduce function to combine the public and private data of a given event
- Take the resulting map and grab the list of
values
The below image should help you to visualize how this all works.
Putting this all together for our use case, we end up with the following code snippet.
return (eventsPublic + eventsPrivate) .groupingBy { it.id } .reduce { _, accumulator: SimilarEvent, element: SimilarEvent -> accumulator.copy( shortUrl = element.shortUrl, isSaved = element.isSaved, ) } .values.toList()
The end result is a list of events that have combined public and private data. Moreover, we’ve been able to open the door for effective caching of the public portion. Problem solved.
Want to try out these Kotlin functions on your own? I’ve created a sample code snippet for you in a Kotlin Playground.