Federation, caching and next steps
Why HTTP caching struggles with GraphQL, response caching by operation, Apollo Federation basics, and where to read next.
Caching a single endpoint
Every request is a POST to one URL, so CDNs and browsers cannot use the URL as a cache key. Caching has to move to two other layers: the client normalises entities by type and id, and the server caches at the operation or resolver level.
| Layer | Key | Invalidation |
|---|---|---|
| Client normalised cache | type plus id | Automatic on mutation result |
| Persisted operation cache | Query hash plus variables | TTL, and bust on deploy |
| Resolver / entity cache | Entity id | Explicit eviction on write |
| CDN with GET and APQ | URL plus query hash | Short max-age, plus purge |
@cacheControl hints | Field-level maxAge | Aggregated into a response header |
import { ApolloServerPluginCacheControl } from "@apollo/server/plugin/cacheControl";
const server = new ApolloServer({
typeDefs,
resolvers,
plugins: [
ApolloServerPluginCacheControl({
defaultMaxAge: 0, // private by default
calculateHttpHeaders: true,
}),
],
});
// type Book @cacheControl(maxAge: 300) { id: ID! title: String! }
// type Query { me: User @cacheControl(maxAge: 0, scope: PRIVATE) }⚠️
Never cache a response that contains user-specific data under a public key. Always set
Cache-Control: private when the response depends on the caller, and treat any shared cache as hostile to correctness unless the key includes the user.Federation in one page
# users subgraph
type User @key(fields: "id") {
id: ID!
name: String!
email: String!
}
# reviews subgraph extends the same entity
type Review @key(fields: "id") {
id: ID!
body: String!
author: User!
}
extend type User @key(fields: "id") {
id: ID! @external
reviews: [Review!]!
}- Each subgraph owns part of a type and contributes fields to it; the router composes one schema for clients.
- An entity is joined by its
@key. The router asks each subgraph for the fields it owns and merges the results by key. - Cross-subgraph joins are the cost: a single client query can become several network calls, so the router caches entity representations aggressively.
- Federation is an organisational tool for independent teams. One team with one service should not pay its complexity.
Where to go next
- Read the specification itself: the October 2021 release covers the semantics of null propagation, defer and stream, and is short enough to read end to end.
- Study one client's normalised cache in depth. Understanding how a mutation result updates cached entities explains most "the UI shows stale data" bugs.
- Build a small schema with a real authorization story before adding federation - authorization across subgraphs is substantially harder.
- Instrument per-field timings and query counts per operation. Those two measurements find more production problems than any schema redesign.
- Keep an eye on incremental delivery (
@deferand@stream): it reduces time to first meaningful paint without a second endpoint.
query Dashboard {
summary { orderCount revenue }
... @defer {
recentOrders { nodes { id total } }
}
}FAQ
Should I start with federation?
No. Start with one schema and one service, and let the domain boundaries become obvious through real ownership. Splitting early means paying composition and cross-service join costs before there is any organisational reason to.
Can I use a CDN with GraphQL at all?
Yes, with persisted queries over GET so the request has a URL and a hash, plus short TTLs for genuinely public data. Anything user-specific must be private or uncached, and the complexity is usually only worth it for high-traffic public content.
Related
DataLoader and solving the N+1 problem Subscriptions and real-time updates
Last refreshed 2026-09-18.