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.

LayerKeyInvalidation
Client normalised cachetype plus idAutomatic on mutation result
Persisted operation cacheQuery hash plus variablesTTL, and bust on deploy
Resolver / entity cacheEntity idExplicit eviction on write
CDN with GET and APQURL plus query hashShort max-age, plus purge
@cacheControl hintsField-level maxAgeAggregated 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 (@defer and @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.

DataLoader and solving the N+1 problem Subscriptions and real-time updates

Last refreshed 2026-09-18.