Menu

Earn Premium with Referrals

Invite your friends and earn Premium rewards through our referral program.

See how it works and start inviting friends.

Pagination
HLD

Pagination

Why every list endpoint needs it from day one — the two families, and how to choose before the data outgrows you.

The Day-One Requirement

Every list endpoint will eventually face a user with a million rows. Without pagination designed in, that first power user becomes your outage. Pagination is not a feature; it is load control for read paths.

 UNPAGINATED                        PAGINATED
 GET /trips → 4M records, 2GB       GET /trips?limit=20 → 40KB
 app server OOMs, DB drowns         bounded memory, bounded latency,
                                    predictable client rendering
 
 rule: NO list endpoint ships without a limit + strategy

Two Families

 OFFSET PAGINATION                  CURSOR PAGINATION
 ?limit=20&offset=100               ?limit=20&cursor=eyJpZCI6MTAwfQ==
 
 "skip 100, give me 20"             "continue after this opaque bookmark"
 
 ✓ random access to page N          ✓ stable under inserts/deletes
 ✓ jump links, page numbers         ✓ no skipped/duplicated items
 ✗ unstable under concurrent        ✗ only forward/sequential walks
   writes (rows shift)              ✗ no "jump to page 37"
 ✗ OFFSET scans past rows           ✓ constant cost at any depth
   → page 10,000 reads 200k rows

The mechanics of each family get their own lessons — this one is about choosing.

The Decision Table

RequirementPick
Admin UI with numbered pagesOffset
Infinite scroll feedsCursor
Data changes during browsing (feeds, orders)Cursor
Deep pagination expected (millions of pages)Cursor
Simple internal tools on small tablesOffset

Consumer-facing infinite-scroll surfaces — most modern products — are cursor territory by default.

API Shape Conventions

 request:
   GET /trips?limit=20&cursor=<opaque>
 
 response envelope:
 {
   "data": [ ...up to 20 trips... ],
   "pagination": { "next_cursor": "eyJ...", "has_more": true }
 }
 
 rules that make clients simple:
 - cursor is OPAQUE base64 — never document its internals;
   you'll want to change encoding later without breaking anyone
 - has_more spares clients an extra empty fetch
 - default limit exists (20); max limit enforced (100)

Defaults and Limits Are Load Control

 unbounded limit=99999999 is a DoS vector:
   one request → full table scan → serialized megabytes
 
 enforce: default_limit = 20
          hard_max     = 100      (4xx beyond)
          response includes actual count served

Interview Framing

“Design the feed API” questions test pagination instinct immediately. Score pattern: state the family choice with reason (“infinite scroll over mutable feed → cursor”), show the envelope once, mention limits enforcement as load protection. Candidates who say “we’d paginate” without picking a family get asked exactly this follow-up — have the decision table ready.

My Private Notes

Notes are auto-saved locally to this device.