Client-side caching


Apollo iOS supports client-side caching of GraphQL response data. Utilizing our caching mechanisms, your application can respond to GraphQL queries using locally cached data that has been previously fetched. This helps to reduce network traffic, which provides a number of benefits including:

  • Shorter loading times

  • Reduction of server load and cost

  • Less data usage for users of your application

Apollo iOS uses a normalized cache that, when configured properly, acts as a source of truth for your graph, enabling your application to react to changes as they're fetched.

The Apollo iOS library contains both a short-lived in-memory cache and a SQLite cache that persists cache data to disk.

Learn about using cache policies to configure how GraphQL operations interact with cache data by reading our documentation on fetching locally cached data.

What is a normalized cache?

In a GraphQL client, a normalized cache breaks each of your GraphQL operation responses into the individual objects it contains. Then, each object is cached as a separate entry based on its cache key. This means that if multiple responses include the same object, that object can be de-duplicated into a single cache entry. This reduces the overall size of the cache and helps keep your cached data consistent and fresh.

Because the normalized cache updates cache entries across all of your operations, data fetched by one operation can update objects fetched by another operation. This allows you to watch your queries and react to changes across your entire application. You can use this to update your UI automatically or trigger other events when new data is available.

Normalizing responses

In order to maintain a normalized cache, Apollo iOS processes response data of your GraphQL operations, identifying each object and creating new cache entries or merging data into existing cache entries.

To understand how Apollo iOS does this, consider this example query:

GraphQL
Query
1query GetFavoriteBook {
2  favoriteBook { # Book object
3    id
4    title
5    author {     # Author object
6      id
7      name
8    }
9  }
10}

The favoriteBook field in this query returns a Book object, which in turn includes an Author object. An example response from the GraphQL server may look like this:

JSON
Response
1{
2  "favoriteBook": {
3    "id": "bk123",
4    "title": "Les Guerriers du silence",
5    "author": {
6      "id": "au456",
7      "name": "Pierre Bordage"
8    }
9  }
10}

A normalized cache does not store this response directly. Instead, it breaks it up into individual cache entries. By default, these cache entries are identified by their path from the root operation. Because this example is a query (rather than a mutation or subscription), the root has the name QUERY_ROOT.

JSON
Cache Entries
1"QUERY_ROOT": {
2  "favoriteBook": "-> #QUERY_ROOT.favoriteBook"
3}
4
5"QUERY_ROOT.favoriteBook": {
6  "id": "bk123",
7  "title": "Les guerriers du silence",
8  "author": "-> #QUERY_ROOT.favoriteBook.author"
9}
10
11"QUERY_ROOT.favoriteBook.author": {
12  "id": "au456",
13  "name": "Pierre Bordage"
14}

The QUERY_ROOT entry is always present if you've cached results from at least one query. This entry contains a reference for each top-level field you've included in any queries (e.g., favoriteBook).

The favoriteBook entry has a author field containing the string "-> #QUERY_ROOT.favoriteBook.author". The -> # indicates that this is a reference to another cache entry, in this case, the QUERY_ROOT.favoriteBook.author entry.

Normalizing objects by their response path allows us to merge changes from other operations along the same response path.

For example, if we defined another query that fetched additional fields on the favoriteBook object, they could be merged into the existing cache entry.

GraphQL
Query
1query FavoriteBookYear {
2  favoriteBook { # Book object
3    id
4    yearPublished
5  }
6}
JSON
Response
1{
2  "favoriteBook": {
3    "id": "bk123",
4    "yearPublished": 1993
5  }
6}

After merging this response into the cache, the favoriteBook entry would have the yearPublished field added to its existing data.

JSON
Cache Entries
1"QUERY_ROOT.favoriteBook": {
2  "id": "bk123",
3  "title": "Les guerriers du silence",
4  "author": "-> #QUERY_ROOT.favoriteBook.author",
5  "yearPublished": 1993
6}

The favoriteBook field can now be queried for its title and yearPublished in a new query, and the normalized cache could return a response from the local cache immediately without needed to send the query to the server.

GraphQL
Query
1query FavoriteBookTitleAndYear {
2  favoriteBook { # Book object
3    title
4    yearPublished
5  }
6}

Normalizing objects by cache key

This section explains how cache keys are used to merge object data in the normalized cache. For information on how to configure your cache keys, see Custom cache keys.

Normalizing response data by the response path helps us de-duplicate responses for the same fields, but it does not allow us to merge cache entries from different fields that return the same object.

In this query, we fetch a Book object using the field at the path bestFriend.favoriteBook.

GraphQL
Query
1query BestFriendsFavoriteBook {
2  bestFriend {
3    favoriteBook { # Book object
4      id
5      title
6      genre
7    }
8  }
9}
JSON
Response
1{
2  "bestFriend" {
3    "favoriteBook": {
4      "id": "bk123",
5      "title": "Les guerriers du silence",
6      "genre": "SCIENCE_FICTION"
7    }
8  }
9}

When this response is merged into the cache, we have new cache entries added for QUERY_ROOT.bestFriend and QUERY_ROOT.bestFriend.favoriteBook.

The response tells use that our bestFriend has the same favoriteBook as us! However, the data for same book is not de-duplicated in our cache entries.

JSON
Cache Entries
1"QUERY_ROOT.favoriteBook": {
2  "id": "bk123",
3  "title": "Les guerriers du silence",
4  "author": "-> #QUERY_ROOT.favoriteBook.author",
5  "yearPublished": 1993
6}
7
8"QUERY_ROOT.bestFriend": {
9  "favoriteBook": "-> #QUERY_ROOT.bestFriend.favoriteBook"
10}
11
12"QUERY_ROOT.bestFriend.favoriteBook": {
13  "id": "bk123",
14  "title": "Les guerriers du silence",
15  "genre": "SCIENCE_FICTION"
16}

If we tried to fetch a query with the field favoriteBook.genre, the cache would not find the genre field on the cache entry QUERY_ROOT.favoriteBook, so it would send the query to the server to fetch the duplicate data.

In order to de-duplicate response data from different fields that return the same object, we need to configure the cache to recognize that they are the same object. We can do that by providing cache key configuration for the Book object.

In this example, the Book object type has an id field that uniquely identifies it. Since our favoriteBook and bestFriend.favoriteBook cache entries have the same id, we know they represent the same Book object. We can configure the cache to use the id field as the cache ID for all Book objects. This will ensure the cache normalizes our cache entries correctly.

To configure cache keys, we return a new CacheKeyInfo value from the SchemaConfiguration.cacheKeyInfo(for type:,object:) function.

Swift
SchemaConfiguration.swift
1static func cacheKeyInfo(for type: Object, object: JSONObject) -> CacheKeyInfo? {
2  switch type {
3  case MySchema.Objects.Book:
4    return try? CacheKeyInfo(jsonValue: object["id"])
5
6  default: return nil
7  }
8}

With this set up, whenever the normalized cache writes response data for a Book object, it will use the id to construct a cache key, instead of the response path.

To prevent cache key conflicts across different object types, the cache prepends the __typename of the object to the provided cache ID followed by a colon (:).

This means the cache key for our Book will now be "Book:bk123".

For more information on using CacheKeyInfo to configure cache keys, see Custom cache keys.

With cache key resolution configured for the Book type, the response data for the queries above would create a single, normalized Book object.

JSON
Cache Entries
1"QUERY_ROOT": {
2  "favoriteBook": "-> #Book:bk123"
3}
4
5"BOOK:bk123": {
6  "id": "bk123",
7  "title": "Les guerriers du silence",
8  "author": "-> #QUERY_ROOT.favoriteBook.author",
9  "yearPublished": 1993,
10  "genre": "SCIENCE_FICTION"
11}
12
13"QUERY_ROOT.bestFriend": {
14  "favoriteBook": "-> #Book:bk123"
15}

The cache entry for BOOK:bk123 contains all of the fields fetched on the Book from all queries. Additionally, the favoriteBook and bestFriend.favoriteBook fields are a cache reference to the entry with the cache key BOOK:bk123.

To learn more about the normalization process, see our blog posts:

Clearing cached data

All caches can be cleared in their entirety by calling clear(callbackQueue:completion:) on your ApolloStore.

If you need to work more directly with the cache, check out Direct cache access.

Feedback

Edit on GitHub

Forums