Episode 20 builds an end-to-end caching architecture: server-side caching with cache warming and invalidation, cache layers from CDN to Redis, Apollo Client's normalized cache with type policies, cache update and eviction patterns, and an introduction to GraphQL CDNs.

Caching is one of the most effective tools for speeding up an application — and also one of the sneakiest sources of bugs. Episode 20 builds a proper end-to-end caching architecture: from the server side to the client side, with an understanding of normalization, invalidation, and eviction. We'll cover server-side caching, the available cache layers, Apollo Client's normalized cache, how to update and evict the cache, and an introduction to GraphQL CDNs.
async function cachedQuery(operation, key) {
const cached = await ctx.redis.get(key);
if (cached) return JSON.parse(cached);
const result = await execute(operation);
await ctx.redis.set(key, JSON.stringify(result), "EX", 60);
return result;
}async function createPost(_, args, ctx) {
const post = await ctx.db.posts.create(args.input);
await ctx.redis.del("feed:home");
await ctx.redis.del(`posts:${args.input.authorId}`);
return post;
}docker run -d -p 6379:6379 redis:7Common practice: combine layers with rules — cache query results for public data on the CDN, authentication and per-user data in Redis (run via docker run -d -p 6379:6379 redis:7), and avoid application-level caches that are hard to invalidate centrally.
The client side has its own cache — this is what makes the UI feel instant. Apollo Client stores query results in a normalized cache, storing each object once based on __typename and id:
import { ApolloClient, InMemoryCache } from "@apollo/client";
const client = new ApolloClient({
uri: "http://localhost:4000",
cache: new InMemoryCache({
typePolicies: {
Query: {
fields: {
posts: {
keyArgs: ["category"],
},
},
},
},
}),
});The normalized cache stores each object once based on __typename and id, then links them — so two queries sharing the same object automatically share data without refetching.
cache-first (default): use the cache when available; network only if empty.cache-only: only the cache, no network.network-only: always the network, ignore the cache.cache-and-network (default for feed UIs): show the cache immediately, then update from the network.Normalization works by combining __typename and id. For types without an id, configure keyFields so they can still be normalized. Without a stable key, the same query will be fetched repeatedly — a common cause of "the cache isn't working" in Apollo Client:
const cache = new InMemoryCache({
typePolicies: {
Review: {
keyFields: ["id"],
},
Person: {
keyFields: ["firstName", "lastName"],
},
},
});Without a stable key, objects aren't normalized and the same query will be fetched repeatedly. This is a common cause of "the cache isn't working" in Apollo Client.
const [createPost] = useMutation(CREATE_POST, {
update(cache, { data }) {
const existing = cache.readQuery({ query: GET_POSTS });
cache.writeQuery({
query: GET_POSTS,
data: {
posts: [data.createPost, ...existing.posts],
},
});
},
});To drop data from the cache (for example, on logout), call cache.evict and cache.gc:
client.cache.evict({ id: `User:${userId}` });
client.cache.gc();Optimistic UI displays temporary changes before the server responds (episode 5): Apollo stores the optimistic result in the cache, then replaces it with the real result when the mutation completes. This gives an instant UI experience.
A GraphQL CDN stores query results at the edge network, drastically lowering latency for global users. Services like Stella (Netlify) and GraphCDN connect directly to a GraphQL origin:
npx graphcdn pushThe configuration is usually a YAML file marking which queries may be cached and the purging rules:
originUrl: https://api.kalian.com
scopes:
- operationName: HomeFeed
maxAge: 60The CDN cache is purged when data changes — usually via a webhook from mutations. This completes the caching architecture: CDN in front, Redis in the middle, and a normalized cache on the client.
Key takeaways:
In the next episode, episode 21, you'll learn about testing GraphQL — the test pyramid, unit testing resolvers with mocks, integration testing with Apollo Server testing and supertest, schema testing with GraphQL Inspector, data mocking, and E2E testing with Cypress. Your API quality will be verified from unit all the way to end-to-end!