Design the API behind a photo feed: pagination, response shape, image variants, caching and what the client needs from it.
Predict first
What do offset paging and cursor paging return when a new post arrives between pages?
func offsetPage(_ feed: [Int], offset: Int, limit: Int) -> [Int] { Array(feed.dropFirst(offset).prefix(limit)) }
func cursorPage(_ feed: [Int], after id: Int?, limit: Int) -> [Int] {
let start = id.flatMap { feed.firstIndex(of: $0) }.map { $0 + 1 } ?? 0 // find the cursor item, continue after it
return Array(feed[start...].prefix(limit))
}
let original = [10, 9, 8, 7, 6, 5, 4, 3, 2, 1] // newest first
let withNew = [11] + original // a new post arrives while the user reads page one
let a1 = offsetPage(original, offset: 0, limit: 3)
let a2 = offsetPage(withNew, offset: 3, limit: 3)
print("offset:", a1, a2)
let b1 = cursorPage(original, after: nil, limit: 3)
let b2 = cursorPage(withNew, after: b1.last, limit: 3)
print("cursor:", b1, b2)
let deleted = original.filter { $0 != 9 } // the cursor item itself is deleted
print("cursor item deleted:", cursorPage(deleted, after: 9, limit: 3), "(falls back to the start)")
- offset: [10, 9, 8] [8, 7, 6] / cursor: [10, 9, 8] [7, 6, 5] / cursor item deleted: [10, 8, 7] (falls back to the start)
- offset: [10, 9, 8] [7, 6, 5]
- cursor: [10, 9, 8] [8, 7, 6]
Mental model. A numbered ticket system with a bookmark: “continue after the ticket I last saw” still works when new tickets are added at the front of the queue, while “skip the first 30” does not.
Reported at Uber (anonymous, unverified). Start with the failure that decides the pagination design: offset paging on a changing list. The user has page one (10, 9, 8); a new post 11 arrives at the top; the next request, offset 3, returns 8, 7, 6, so item 8 appears twice (the program prints exactly this). A cursor avoids it: the next request says “give me 3 items after the last one I have”, and returns 7, 6, 5 whatever was added in front. Make the cursor opaque and have it encode the sort key (for example timestamp plus id), not just an id lookup: the check also shows the weakness of a lookup-by-id cursor, because when the cursor item was deleted the naive version fell back to the start and repeated items. Return nextCursor and a hasMore flag so the client never guesses.
The rest of the contract serves the client. A response shape with stable ids, author summary, counts and image variants (several widths with their URLs and dimensions, so the client picks one without a second call and can reserve the right height before the image loads). Caching: ETag or a cursor for “anything new?” so an empty refresh costs almost nothing, and CDN-friendly, immutable image URLs with a version in them. Writes: likes and comments as idempotent operations with an idempotency key so a retry does not double count (Q493). Rate limits with 429 and Retry-After (see the rate-limiter question), field selection to keep payloads small (Q495), and versioning so old app versions keep working. Say who owns ranking: the server, so the client must handle a feed that is re-ordered between pages (see the feed client question).
Offset paging repeats item 8 when a post is inserted in front; cursor paging does not, but a cursor that depends on an item lookup breaks if that item is deleted.
Common traps
- Offset-and-limit paging on a feed that gains new items. Items repeat or are skipped.
- A cursor that is just “the id of the last item”. It breaks when that item is deleted or moved; encode the sort position.
They might follow up with
- How does the client show “new posts”? Poll or push a lightweight “anything newer than X?” call, and prepend only when the user asks.
- What goes in the image variants? URL, width, height, and a dominant colour or blur hash for the placeholder.
Related questions
- 26.1.4 Design a shopping app with cart and checkout: catalogue, cart merging, price authority, payment, idempotency…
- 26.2.2 How do you set payload budgets and reduce bytes: field selection, deltas, compression, image sizes, and what…
- 26.5.1 Design the client of a social feed: cursor paging over a re-ranked list, de-duplication, a “new posts” pill…
- 26.8.1 Design an API rate limiter, then say how an iOS client should behave under one. Compare token bucket with a…
← Design the rider app of a ride-hailing service: requesting, matching…