A marketplace of eBay's kind ranks its feed per user and re-ranks continuously as listings end and prices change, so an item's position can move within seconds. Users scroll infinitely on mobile. Which pagination approach should the API use?
Show the full answer Hide the answer
The deciding property
The ordering is not stable between requests. Every pagination scheme that computes position from the data assumes the data's order holds still long enough to page through it, and a continuously re-ranked feed breaks that assumption within seconds.
Once the ordering moves, the two user-visible defects are unavoidable in any position-based scheme: items appear twice because they moved down past the cursor, and items are never seen because they moved up past it. On an infinite scroll, duplicates are the visible symptom and the silent misses are the expensive one — inventory the user would have bought.
The fix is to make the ordering stable for the duration of the session: rank once, materialise the ordered list of ids, and page through that. The list lives in a fast store with a short expiry, keyed by session, and the cursor is an offset into an immutable snapshot rather than a predicate over live data.
Why the others fail
- Offset and limit on the ranked query. The worst option here, and the most commonly shipped. It re-runs the ranking for every page, so page 2 is computed from a different order than page 1, and it also gets slower as the offset grows because the database must produce and discard the skipped rows.
- A cursor on rank and id. Better, and it appears correct because cursors are the standard advice. It still fails, because the cursor's predicate refers to a rank value that changes: an item whose rank improved after you passed it is now "before" your cursor and will never be returned. The improvement over offset is performance, not correctness.
- Keyset pagination on creation time. Correct, stable, efficient, and it abandons the ranking, so the feed becomes reverse-chronological. Fine for a timeline where recency is the ranking; wrong when relevance is the product.
What the snapshot costs, and how to bound it
| Concern | Handling |
|---|---|
| Memory | Store ids only, cap the snapshot at a few hundred to a few thousand, and re-rank when the user reaches the end |
| Staleness | Hydrate item details at read time from the live store, so prices and availability are current even though position is frozen |
| Sold-out items | Filter at hydration and backfill from the snapshot so page sizes stay even |
| Expiry | Minutes, not hours; a returning user gets a fresh rank, which is also what they expect |
| Cost | One ranking computation per session instead of one per page, which is usually cheaper than the alternative |
Freezing the order while keeping the data live is the specific trick, and it is what makes the compromise acceptable to users: positions are stable, facts are current.
What would flip the decision
- A feed that is not personalised and changes slowly — keyset pagination on a stable sort key, and no snapshot.
- A strictly chronological product — keyset on time, which is simpler and better.
- A page-numbered search results UI rather than infinite scroll — the snapshot becomes more important, because users navigate back to page 2 and expect the same page 2.
- A tiny catalogue, a few hundred items — send the whole ranked list once and paginate in the client.
When this is over-engineering
If the feed is re-ranked daily rather than continuously, a cursor on rank and id is correct and far simpler. The snapshot exists to absorb ordering churn, so if the ordering does not churn within a session, do not build it. The threshold is whether a user scrolling for two minutes can plausibly see the order change, which is a product question you can answer by measuring how often the top 100 ids change in 120 seconds.