skip to content

Why does a client cache usually hold a paginated list as one entry with many pages rather than one entry per page?

level: middleimportance: should knowfreq 38%

answer

  1. the entry should match what renders
  2. one clock for the whole list
  3. pages are a chain, not siblings
  4. offset shifts, cursors chain
  5. refetch cost grows with depth

basics

~20 s

The accumulated list is what the user sees, so it needs one loading state, one refetch and one invalidation. Pages are not independent either: a cursor page depends on the previous response, and offset pages shift when rows are inserted.

solid answer

~40 s

The unit a cache stores should match the unit a screen renders, and for a scrolling list that is the accumulated result of one query, not each slice. Holding it as a single entry containing an ordered array of pages gives one staleness clock, one loading and error state, and one invalidation, so a refresh cannot leave page one current and page three ten minutes old. It also reflects how pages were obtained: with cursor paging each request needs the marker the previous response returned, so pages form a chain and cannot be refetched independently. With offset paging an insert or delete shifts rows between requests, so independently refreshed pages duplicate or skip items. One entry costs as many requests as pages loaded, so caches usually cap retained pages.

go deeper

for a junior

Recall that a scrolling list is cached as one thing with several pages inside it, because the user sees the whole accumulated list and it should load, fail and refresh as a unit.

for a middle

Explain the dependency between pages: cursor pages chain off the previous response, offset pages shift when rows change, so pages cannot be refreshed independently once several are on screen.

for a senior

Decide what a refresh means at depth, handle a failure partway through, cap retained pages, and deduplicate by identity where the API can only offer offsets.

for a principal

Own the contract with the API. Push for stable ordering and markers rather than working around shifting offsets in every client, and set the memory and request budget a deep list may consume.

## What the entry is supposed to represent A cache of server data is keyed by what was asked. For a paginated list, the question the user actually asked is `show me this filtered, sorted list`, and the screen renders the accumulated answer. The pagination parameters are how the client gets that answer in pieces, not a different question per piece. So the natural entry is one per query, holding an ordered collection of pages, each page being one response with its items and whatever paging information came with it. ## What one entry buys - **One loading and error state.** The screen distinguishes `loading the list` from `loading more`, and a failure while appending does not put the whole list into an error state. - **One staleness clock and one invalidation.** Marking the list untrusted marks all of it, so a revalidation cannot leave early pages current and later pages old. Mixed-age pages are how lists come to show an item twice, or to show a removed item below a fresh page that no longer contains it. - **Order is owned by the entry.** The sequence of pages, and therefore of items, is data in its own right. Storing pages separately means something else must remember how to stitch them back, and that something usually forgets after a refetch. - **Refetching in the right shape.** Revalidating the list means re-reading the pages already loaded, in order, and replacing them together, so the user keeps roughly the same scroll depth instead of being reset to one page. ## Why pages are not independent | paging style | how the client asks for the next slice | consequence for the cache | |---|---|---| | offset or page number | skip a number of rows, take a count | any page is directly requestable, but inserts and deletes between requests shift rows, so independently refreshed pages can duplicate or skip items | | cursor or token | pass the marker returned with the previous page | pages form a chain; page three cannot be requested without page two's response, so refetching means walking forward from the start | This is the mechanical reason a per-page entry design breaks with cursor paging. There is no stable key for `page 3` at all, because the request that produces it is only defined relative to the previous response. Keying by the cursor value instead produces an unbounded family of entries, one per marker the server has issued, and those are not reliably reachable again once a refresh changes the markers. Offset paging keeps an addressable key, which is why a classic numbered-pages screen can get away with one entry per page: the user sees exactly one page at a time, a stale neighbour is never on screen next to a fresh one, and a page number survives a reload. The one-entry design earns its place when pages are *accumulated* on screen. ## The costs to plan for 1. **Refetch cost grows with depth.** Ten loaded pages mean ten requests to revalidate honestly. Common mitigations are refreshing only the first page and truncating the rest, capping how many pages are retained, or refreshing on demand rather than on every trigger. 2. **Memory grows with scroll depth**, and the entry is retained after the screen unmounts for as long as the retention window allows. A deep session on several lists is a real memory cost on a low-end device. 3. **Partial failure needs a decision.** If page four fails during a full revalidation, the sensible outcome is usually to keep the pages that succeeded, report the failure, and let the user retry the tail rather than discard the list. 4. **Duplicates are still possible.** Concurrent inserts mean the same item can appear in two pages even when they are fetched together. Screens that must not show a duplicate deduplicate by identity when they flatten pages for rendering. ## A note on where this sits This is only the cache-shape question: what the entry holds, what the key is, and what a refetch means. How many items to request, whether a screen appends or replaces, and how to render very long lists efficiently are separate concerns with their own answers; the cache's contribution is to make the loaded result one addressable, consistently aged thing.

  • Why can a numbered-pages screen store one entry per page, while an accumulating list should not?
    Because only one page is on screen at a time, so two pages of different ages are never rendered together, and an offset key is directly requestable and survives a reload. An accumulating list renders many pages at once, so mixed ages become visible duplicates or gaps, and cursor-based pages are not independently addressable at all.
  • What is a reasonable way to revalidate a list with ten pages loaded?
    Either walk the pages in order and replace them together, accepting ten requests, or refresh the first page and drop the rest so the list is short but coherent. The one thing to avoid is refreshing some pages and keeping others, since that mixes ages inside one visible list and produces duplicated or missing rows.
  • Items appear twice after loading a later page. What is the cause and where do you fix it?
    Rows shifted between requests because something was inserted above the window, so the same item fell into two slices. Cursor-style markers avoid most of it; where the API only offers offsets, deduplicate by identity when flattening pages for render, and treat a duplicate rate that keeps climbing as a signal the ordering key is not stable.

saying these in an interview costs you the question

  • Treats each page as an independent cache entry with its own clock
  • Believes offset paging is stable while rows are being inserted
  • Keys entries by cursor values and expects them to be reusable
  • Refreshes only some pages of a list on screen
  • Forgets that retained pages keep growing with scroll depth
  • Discards the whole list when one page of a refresh fails