Why does a normalized client cache store one entry per argument set for a paginated list field?
answer
- Same field name, different answers
- The name alone is not enough
- Arguments are part of a field's identity
- A cursor argument mints a new entry
basics
~20 sA cached field is stored under a key made of the field name plus the arguments it was fetched with, because different arguments mean a different answer. Each page, fetched with a different cursor argument, therefore lands in its own entry.
solid answer
~50 sA normalized client cache does not store responses whole. It stores each object under an identity and each field under a **storage key**, and that key is the field's name plus the arguments it was called with, because a field's value is a function of its arguments. `payStatements(first: 25)` and `payStatements(first: 25, after: "st_8842")` are two different questions, so the cache keeps two entries. For a paginated list that is exactly what trips people up: page two does not extend page one, it creates a sibling entry, and a view whose variables now carry the cursor reads only the second page. The items themselves are usually not duplicated — normalized entities are shared by identity — but the ordered list of references is per-argument-set. None of this is in the GraphQL specification; argument-aware field keys are a client-cache convention.
code
graphql · 8 linesquery Statements($after: String) {
employee(id: "emp_4471") {
payStatements(first: 25, status: PAID, after: $after) {
edges { node { id periodEnd netPay } }
pageInfo { endCursor hasNextPage }
}
}
}go deeper
Be ready to state the key: field name plus arguments. Then show you can predict the symptom — clicking load more and watching the list appear to be replaced rather than extended.
Explain the mechanics: arguments are serialized into the key, keys hang off the parent record, and entities are shared by identity while reference lists are not. Say which parts are convention rather than specification.
Show you can diagnose from the symptom alone — a list that resets on load more, or a load more that appears to do nothing — and name the two levers that fix it: key-argument selection and a merge rule.
Own the position that argument-keyed fields are correct by default and pagination is the exception. Be able to say what you would standardise across teams so every paginated field on the graph is cached the same way.
## What a storage key is A normalized client cache does not file a response whole. It breaks the response apart: objects that carry an identity are stored once under that identity, and every other place they appear becomes a reference to that record. The fields themselves still have to live somewhere, and they live on the parent record under a **storage key**. The storage key is not just the field's name. It is the field name plus the arguments the field was called with, because a field's value is a function of its arguments. In a payroll and benefits graph: ```graphql query Statements($after: String) { employee(id: "emp_4471") { payStatements(first: 25, status: PAID, after: $after) { edges { node { id periodEnd netPay } } pageInfo { endCursor hasNextPage } } } } ``` `payStatements(first: 25, status: PAID)` and `payStatements(first: 25, status: VOIDED)` are two different questions. A cache that stored both under the bare name `payStatements` would hand the second answer to a caller who asked the first. So the entry written on the `employee(id: "emp_4471")` record is closer to `payStatements({"first":25,"status":"PAID"})` — the arguments serialized in a canonical order and folded into the key. ## The specification's own version of the same idea The GraphQL specification has nothing at all to say about client caches; normalization and argument-aware keys are a client convention. It does, though, contain the same intuition, as a document validation rule: within a single selection set, two fields sharing a response name must have the same field name **and the same arguments**, or the document is invalid. `payStatements(first: 25)` and `payStatements(first: 50)` cannot both occupy the response key `payStatements` in one response object, because the executor would have to merge two selections whose answers legitimately differ. A cache meets the identical problem across time rather than inside one document, and resolves it the same way: the arguments are part of the field's identity. ## Why pagination is where this bites Cursor pagination works by sending the same field again with one argument changed — the cursor that marks where the last window ended. That single changed argument produces a different storage key. The second page is therefore not an extension of the first; it is a brand-new entry, sitting beside it on the same parent record. What the user sees depends on what the view reads. A view whose variables now carry `after: "st_8842"` reads the entry for those arguments and finds only the twenty-five statements of page two, so the list appears to have been replaced rather than extended. A view still bound to the cursor-less variables reads the first entry and never sees the new data at all, so "load more" looks like it did nothing. Both symptoms have the same cause, and neither is data loss: page one is intact under its own key, and navigating back to the cursor-less variables renders it instantly. ## The items are shared; the list is not It is worth being precise about what is duplicated. If the individual statements carry identities and are normalized, each one is stored once no matter how many pages reference it; a re-delivered item updates the existing record in place rather than creating a second copy. What multiplies is the ordered list of references, one per argument set. Seventy-four pages of twenty-five statements do not store the employee seventy-four times — but they do leave seventy-four reference lists behind, and no single entry holds the whole list a scrolling view wants to render. ## Variables, literals and defaults The key is computed from argument **values**, after variables have been substituted, so sending `after: $after` with `$after = "st_8842"` keys the same as writing the literal in the document. The edge case worth knowing is the optional argument: whether omitting an argument and passing it explicitly as its schema default produce the same key is a decision each cache implementation makes, not something any specification settles. Two call sites that differ only in that way can quietly land in two entries. ## Living with it Nothing above is a bug — argument-keyed fields are what makes a cache safe. The problem is only that pagination arguments describe a *window* rather than a *list*, so they fragment something the user experiences as one thing. Client caches therefore expose two levers, both conventions rather than specified behaviour: a declaration of which arguments actually belong in the key, so the window arguments can be left out, and a merge rule that folds an incoming window into the single accumulating entry that remains. Each lever has its own failure mode, and both have to be set deliberately — the default of keying on every argument is correct in general and wrong for exactly this shape of field.
- If the cursor is part of the storage key, is the first page lost once page two is fetched?No. Page one is still stored under its own key, untouched. What changed is which entry the view reads: with a cursor in its variables it reads the page-two entry and sees only those items. Navigate back to the cursor-less variables and page one renders instantly from cache. Both entries occupy memory until something evicts them.
- Are the individual items stored twice when two pages overlap?Not if they are normalized. An item with an identity is stored once as a record, and a second delivery updates that record's fields in place rather than creating a copy. The duplication lives in the reference lists: the same item id can appear in two different argument-keyed entries, each holding a reference to the one record.
- Does the GraphQL specification say how a client should key cached fields?No — the specification has no notion of a client cache at all. It defines response keys within a response object and a validation rule that two selections sharing a response name must have identical field names and arguments. Storing a field under name-plus-arguments is a client-cache convention that happens to rest on the same reasoning.
The arguments are part of the question, so the cache files the answer under the whole question rather than under its first word.
saying these in an interview costs you the question
- Says a cache keys fields by name alone
- Thinks page two overwrote page one's data
- Believes the GraphQL spec defines the client cache key
- Assumes an item is stored once per page it appears in
- Expects a changed cursor to reuse the same entry automatically