Apollo Kotlin Normalized Cache Help

Compiler plugin

When setting up the Normalized Cache in your project, you need to configure the compiler plugin:

// build.gradle.kts apollo { service("service") { // ... // Add this plugin("com.apollographql.cache:normalized-cache-apollo-compiler-plugin:1.0.0-beta.1") { argument("com.apollographql.cache.packageName", packageName.get()) } } }
// For Apollo Kotlin v5 and later // build.gradle.kts apollo { service("service") { // ... // Add this plugin("com.apollographql.cache:normalized-cache-apollo-compiler-plugin:1.0.0-beta.1") pluginArgument("com.apollographql.cache.packageName", packageName.get()) } }

This plugin generates code to support the Normalized Cache features, such as declarative cache IDs, pagination and expiration.

Declarative cache IDs (@typePolicy)

You can refer to the declarative cache IDs documentation for a general overview of this feature.

Here are some additional details of what the compiler plugin does to support it.

Let's consider this schema for example:

# schema.graphqls type Query { user(id: ID!): User } type User { id: ID! email: String! name: String! }
# extra.graphqls extend type User @typePolicy(keyFields: "id")

Generation of typePolicies

A map of type names to TypePolicy instances is generated in a Cache object.

In the example above, the generated code will look like this:

object cache { val typePolicies: Map<String, TypePolicy> = mapOf( "User" to TypePolicy(keyFields = setOf("id")) ) }

This map is passed to the TypePolicyCacheKeyGenerator when calling the cache() extension.

If you need more control over the configuration, use the normalizedCache() extension and pass this map to the TypePolicyCacheKeyGenerator:

val apolloClient = ApolloClient.Builder() // ... .normalizedCache( // ... cacheKeyGenerator = TypePolicyCacheKeyGenerator(Cache.typePolicies) ) .build()

Addition of key fields and __typename to selections

The compiler automatically adds the key fields declared with @typePolicy to the selections that return that type. This is to ensure that a CacheKey can be generated for the record.

When you query for User, e.g.:

# operations.graphql query User { user(id: "1") { email name } }

The compiler plugin will automatically add the id and __typename fields to the selection set, resulting in:

query User { user(id: "1") { __typename # Added by the compiler plugin email name id # Added by the compiler plugin } }

Now, TypePolicyCacheKeyGenerator can use the value of __typename as the type of the returned object, and from that see that there is one key field, id, for that type.

From that it can return User:42 as the cache key for that record.

Unions and interfaces

Let's consider this example:

# schema.graphqls type Query { search(text: String!): [SearchResult!]! } type Product { shopId: String! productId: String! description: String! } type Book { isbn: ID! title: String! } union SearchResult = User | Post
# extra.graphqls extend type Product @typePolicy(keyFields: "shopId productId") extend type Book @typePolicy(keyFields: "isbn")
# operations.graphql query Search($text: String!) { search(text: $text) { ... on Book { title } } }

The plugin needs to add the key fields of all possible types of SearchResult, like so:

query Search($text: String!) { search(text: $text) { __typename # Added by the compiler plugin ... on Book { title } # Added by the compiler plugin ... on Book { isbn } ... on Product { shopId productId } } }

The principle is the same with interfaces, for instance:

# schema.graphqls type Query { search(text: String!): [SearchResult!]! } interface SearchResult { summary: String! } type Product implements SearchResult { summary: String! shopId: String! productId: String! } type Book implements SearchResult { summary: String! isbn: ID! title: String! }

The modified query would look the same as above, with the key fields of Product and Book added to the selection set.

Resolving to cache keys (@fieldPolicy)

When a field returns a type that has key fields, and takes arguments that correspond to these keys, you can use the @fieldPolicy directive.

For instance,

# schema.graphqls type Query { user(id: ID!): User } type User { id: ID! email: String! name: String! }
# extra.graphqls extend type User @typePolicy(keyFields: "id") extend type Query @fieldPolicy(forField: "user", keyArgs: "id")

From this, when selecting e.g. user(id: 42) the FieldPolicyCacheResolver knows to return User:42 as a CacheKey, thus saving a network request if the record is already in the cache.

Unions and interfaces

If a field returns a union or interface it is not possible to know which concrete type will be returned at runtime, and thus prefixing the cache key with the correct type name is not possible. A network call can't be avoided here.

However, if your schema has ids that are unique across the service, you can pass CacheKey.Scope.SERVICE to the cache() extension or FieldPolicyCacheResolver constructor to skip the type name in the cache key. Network call avoidance will work in that case.

cache() extension function

An ApolloClient.Builder.cache() extension function is generated by the compiler plugin, which configures the CacheKeyGenerator, MetadataGenerator, CacheResolver, and RecordMerger based on the type policies, connection types, and max ages configured in the schema:

val apolloClient = ApolloClient.Builder() // ... .cache(cacheFactory = /*...*/) .build()

Optionally pass a defaultMaxAge (infinity by default) and keyScope (CacheKey.Scope.TYPE by default).

Last modified: 15 September 2025