Cheat sheet · Practical Design Problems: From Whiteboard to Working Code
Practical Design Problems cheat sheet
The six-step frame, the numbers worth knowing, and the API, data model and core trick of each classic design problem. Lead the talk, use numbers, and always say what fails.
blogs · jp-blogs.vercel.app/cheatsheets/series/design-problems
The six-step frameLesson 0 →
- Clarify: what, who, out of scope. Functional and non-functional.
- Estimate: requests per second, storage, read:write ratio.
- API: methods, status codes, errors, auth, pagination, idempotency.
- Data model: what is stored, how it is read, keys and indexes.
- Core design: the hard part, with alternatives and a choice.
- Scale and fail: 10x traffic, a dependency down, hot keys, abuse.
Tip: 45 min: 5 clarify and estimate, 10 API and data, 15 core, 10 scale and failure, 5 wrap-up. Say: "A gives X but costs Y; given Z I choose A."
Back-of-envelope numbersLesson 0 →
| Fact | Approximation |
|---|---|
| Seconds in a day | 86,400, about 10^5 |
| 1 million requests a day | about 12 per second |
| 100 million a day | about 1,200 per second |
| Peak traffic | Average x 2 to 10 (use 3 to 5) |
| 1M rows x 1 KB | about 1 GB (1B rows is 1 TB) |
| Latency | Memory 100 ns, Redis 0.5 ms, SSD 100 us, region hop 100 ms+ |
writes: 100M/month = 40/s (peak 200)
reads 100x = 4,000/s (peak 20,000)
6B links x 500 B = 3 TB
hot 20%: 20M x 500 B = 10 GB
(one Redis node)Then say the conclusion: writes are small, reads are the problem.
URL shortener: codesLesson 1 →
| Approach | Collisions | Catch |
|---|---|---|
| Hash + truncate | Yes, handle them | Same URL gets same code for every user |
| Random + retry | Rare; retry on unique error | Simple. Watch how full the space gets |
| Counter + Base62 | None | Guessable unless scrambled; needs ranges |
| Key generation service | None | Another service to run |
- Base62 (
0-9a-zA-Z) is URL-safe. 7 characters give 62^7, about 3.5 trillion; 6B links use 0.17%. - Base64 has
+,/and=, which are not URL-safe. - Retry loop: insert, catch Prisma
P2002(unique violation), make a new code, up to 5 tries.
URL shortener: redirect and readsLesson 1 →
| Aspect | 301 Moved Permanently | 302 Found |
|---|---|---|
| Browser cache | Strong; may skip your server | Not cached by default |
| Analytics | Lost on repeat clicks | Every click counted |
| Change or disable link | Hard (stale cache) | Easy, instant |
- Analytics, expiry or edits: 302. Permanent links, least load: 301.
- Cache-aside in Redis with TTL no longer than the link's life. Negative-cache unknown codes (60 s) against scanners.
- Statuses:
404unknown,410expired,409alias taken,429rate limited. - Log clicks to a queue, never in the redirect request.
Watch out: Abuse: allow only http and https, check blocklists, block SSRF if you fetch the target, reject reserved aliases (api, admin), do not number codes in order.
Rate limiter: designLesson 2 →
| Algorithm | Memory per key | Note |
|---|---|---|
| Fixed window | 1 counter | 2x burst at the boundary |
| Sliding log | Many timestamps | Exact, costly |
| Sliding counter | 2 counters | Smooth estimate; good default per minute |
| Token bucket | 2 values | Controlled bursts; usual API default |
| Leaky bucket | Queue | Smooth output for a constant-rate service |
- A limiter is key + rule + algorithm + shared state + response.
- Rate limit counts per period; quota counts a long period; concurrency limit counts running now; load shedding protects the system.
- Place it at the edge or gateway first, then app middleware for per-user rules.
Rate limiter: correct and visibleLesson 2 →
-- ARGV: capacity, rate, nowMs, cost
tokens = min(capacity,
tokens + elapsed * rate)
if tokens >= cost then
tokens = tokens - cost
allowed = 1
end
HSET key tokens ts
PEXPIRE key (idle keys vanish)Run it as one Lua script so check-and-update is atomic. INCR then EXPIRE is not atomic.
429 Too Many Requests
Retry-After: 18
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0- Fail open for most APIs; fail closed for login, OTP, reset. Choose per rule, with a short timeout.
- Users behind one NAT: key on user id or API key, IP only as a wide outer layer. Trust
X-Forwarded-Forfrom trusted proxies only.
Pagination: offset vs cursorLesson 3 →
| Aspect | Offset | Cursor (keyset) |
|---|---|---|
| Cost | Grows with offset, O(offset) | Fast at any depth |
| Data changes | Duplicates and skips | Stable |
| Jump to page N | Yes | No |
| Total count | COUNT(*), slow | Not cheap |
SELECT * FROM orders
WHERE (created_at, id) < ($1, $2)
ORDER BY created_at DESC, id DESC
LIMIT 21; -- limit + 1: hasMoreNeeds index (created_at DESC, id DESC) and a unique tiebreaker.
Pagination: cursors and choosingLesson 3 →
- Cursor is opaque base64url JSON. Validate it strictly, use parameterised queries; sign it with HMAC if needed.
- Put a fingerprint of sort and filters in the cursor. Keep full timestamp precision.
- Cap
limit, set a default, cap offset depth. Backwards: flip comparison and sort, then reverse. - Counts: approximate, cached, a counter table, or "10,000+".
| Situation | Choose |
|---|---|
| Small table, admin page jumps | Offset |
| Feed, timeline, log, big table, public API | Cursor |
| Infinite scroll, Load more | Cursor |
| Ranked search results | search_after |
File uploadLesson 4 →
- Browser asks the API to upload (name, type, size). API validates, saves a
PENDINGrow, returns a presigned URL. - Browser
PUTs the bytes straight to object storage. Your servers stay out of the data path. - Storage event goes to a queue. Worker verifies, scans, makes thumbnails, sets
READYorREJECTED. - Client polls or gets a push. A cleanup job removes stale
PENDINGrows.
- Large files: multipart, parts 5 MB to 5 GB, up to 10,000 parts, 5 TB object. Parallel parts, retry each, resume with
ListParts. - Add lifecycle rule
AbortIncompleteMultipartUpload. CORS must exposeETag.
Watch out: Timeouts behind the load balancer mean bytes pass through it. Move to presigned direct upload.
File upload: validate and serveLesson 4 →
| Check | How |
|---|---|
| Size | content-length-range in policy, recheck after |
| Type | Allow-list plus magic bytes; header and extension lie |
| Name | Server-made key, never the client name |
| Content | Malware scan; re-encode images; cap pixels |
| Archives | Cap unpacked size and nesting (zip bomb) |
- Serve from a separate domain, with
X-Content-Type-Options: nosniffandContent-Disposition: attachment. - Private files: private bucket and short-lived signed GET URLs.
Notification systemLesson 5 →
| Transport | Use |
|---|---|
| Polling | Simple; wasteful, delayed |
| Long polling | Lower delay; still a connection each |
| SSE | Server to client only; auto reconnect, Last-Event-ID |
| WebSocket | Two-way; sticky routing and heartbeats |
- Pipeline: events, bus, notification service (recipients, preferences, dedupe, template), one queue per channel, workers.
- Table has
UNIQUE (user_id, dedupe_key), a partial index on unread, anddeliveries (notification_id, channel). - Queues are at-least-once, so dedupe key plus idempotent senders plus DLQ.
- Digest, quiet hours (user time zone), frequency caps, critical messages always allowed.
Notifications: scenariosLesson 5 →
- 3 tabs open
- 3 SSE connections; or a leader tab via
BroadcastChannel; use HTTP/2 - Broadcast to 1M
- Fan-out on write (batched, throttled) or on read; hybrid; FCM topics
- Offline user
- Saved in-app list, Web Push, email or SMS fallback
- Estimate
- 20M a day, about 230/s (peak 2,000), 100,000 open connections
- The bell
- List + unread count + stream; optimistic read;
aria-live="polite"
Search autocompleteLesson 6 →
- Minimum 2 characters. Debounce 150 to 300 ms.
- Check the per-prefix cache. On a miss, abort the previous request and fetch.
- Ignore stale responses (compare the returned
query). Cache by prefix. - Combobox ARIA, arrow keys, Enter, Escape. Handle IME. Highlight safely.
| Backend option | Note |
|---|---|
| SQL LIKE 'pre%' | Index with text_pattern_ops; ranking is weak |
| Search engine | Typo tolerance, ranking; heavier |
| Trie, top-k at each node | Lookup is O(prefix length); build offline |
| Hash map prefix to top-k | Cap prefix length (20) |
Tip: Serve GET /v1/suggest with Cache-Control: public, max-age=300 at the CDN. Fail softly: hide suggestions if slow.
Payments, retries, flags, LRULesson 7 →
- Idempotency key per intended payment, unique on
(account_id, key), with request hash. Same key + same hash returns the saved response; different hash is422; in progress is409. - Pass the key to the provider too. A timeout means unknown, not failed. Payments are a state machine with conditional
UPDATE. - Full jitter:
delay = random(0, min(cap, base * 2^attempt)). Retry temporary errors on idempotent calls only; one layer only. - Flag:
hash(flagKey + userId) % 100 < percent. Same answer every time; raising 10 to 20 keeps the first 10%. - Flags: evaluate locally, safe default, kill switch, audit log, owner and end date. Not for authorization.
get(k): v = map.get(k)
map.delete(k); map.set(k, v)
put(k, v): map.delete(k)
map.set(k, v)
if (map.size > cap)
delete map.keys().next().valueA Map keeps insertion order, so the first key is the least recently used.
Interview one-linersLesson 0 →
| Question | Short answer |
|---|---|
| 301 or 302? | 302 for analytics and control; 301 for permanent and light. |
| Offset or cursor? | Cursor for feeds and big data; offset for small admin tables. |
| Why presigned URLs? | Bytes skip your servers: no double bandwidth, no LB timeouts. |
| Exactly-once? | At-least-once delivery plus idempotent handlers. |
| Fail open or closed? | Open for most APIs; closed for login, OTP and reset. |
| Why jitter? | Clients that failed together would retry together. |
| Why a cursor tiebreaker? | Equal sort values make the page edge ambiguous. |
A cheat sheet is a reminder, not a teacher. Each card links to the lesson that explains it step by step.