Classic design prompts, answered with primitives you already own
Rate limiter, shortener, notifications, feed, chat, webhooks, scheduler, uploads, autocomplete — mapped onto code you have actually shipped.
Concept → your projects
Every design interview eventually converges on a handful of prompts. You already own the primitives for all of them, which is the entire advantage of having built two systems instead of reading about ten. For each prompt below: the shape of the problem, the primitive you have shipped, the deep dive that earns the offer, and the trap.
1. Rate limiter
Shape: limit requests per identity per window across N instances; degrade gracefully; never block the critical path.
Yours: Zoth's per-tenant limits; Traverse's auth limiter (and its known flaw — in-memory, so it breaks at two replicas, Day 50).
Deep dive: token bucket vs fixed window vs sliding window log vs sliding window counter. Token bucket for burst tolerance, sliding counter for smoothness. Where state lives: Redis with INCR + EXPIRE is the cheap version; a Lua script makes check-and-decrement atomic (otherwise you have a read-modify-write race — the same class of bug as Day 32). Fail-open vs fail-closed is a product decision: for auth, fail-closed; for a read endpoint, fail-open and log.
Trap: saying "use Redis" and stopping. The interviewer wants the atomicity story and the multi-instance story.
2. URL shortener
Shape: map short code → long URL, redirect fast, never collide, analytics. Yours: the ID-generation discussion from Day 20 — UUIDv4 vs UUIDv7 vs ULID vs Snowflake vs sequence. Deep dive: base62 of a counter vs random-with-collision-retry vs hash-then-truncate. Sequential leaks volume and is enumerable; random needs a uniqueness check (a unique index and a retry loop, not a read-then-write). Read path: cache-aside in Redis with a long TTL because the mapping is immutable (Day 44). Write path: the redirect is the hot path, so analytics must be async — you already have this pattern (enqueue an event, aggregate later). Trap: forgetting that the read:write ratio is ~1000:1, which makes this a caching question more than a database question.
3. Notification service
Shape: fan out an event to many users over many channels, with retries, dedup, and no duplicate spam.
Yours: Traverse's notification system — an intent row (Notification) plus a per-channel ledger (NotificationDelivery) claimed with SKIP LOCKED under a 120-second lease, with backoff and terminal statuses (Day 61).
Deep dive: separate intent from delivery attempt. Intent is the product truth ("the user should know about this"); delivery is a per-channel state machine (pending → claimed → sent → failed → dead). Idempotency comes from a dedup key on intent, not from hoping the provider is idempotent. Quiet hours, per-channel preferences, and rate caps are policy applied at intent time. Push is best-effort; in-app is the source of truth — because you cannot promise delivery on a channel you do not control.
Trap: treating "send a push" as one step. It is at minimum four.
4. News feed
Shape: each user sees posts from people they follow, newest-ish, fast.
Yours: the social-graph work (Day 74) — double-row friendships, visibility predicates, cursor pagination.
Deep dive: fanout on write (precompute each follower's timeline into a store; fast reads, expensive writes, brutal for celebrities) vs fanout on read (query the followee set at read time; cheap writes, expensive reads, needs an index that matches the query shape) vs hybrid (fanout on write for normal accounts, merge-on-read for the top N accounts). Pagination must be a cursor (stable key + tiebreaker), never OFFSET — the same lesson as Traverse's query-shape fixes (Day 28).
Trap: not asking about the celebrity/hot-key case. That question is the whole prompt.
5. Chat / messaging
Shape: two users exchange messages with ordering, delivery, and history. Yours: Traverse's push-not-polling decision and Traverse's client architecture (Day 73) — plus the honest note that you deliberately avoided a stateful socket fleet. Deep dive: ordering needs a per-conversation sequence (a monotonic counter per conversation, not a global timestamp — clocks lie). Delivery semantics: at-least-once plus a client-side dedup on message ID beats exactly-once, which does not exist. Presence is ephemeral state and belongs in Redis with TTLs, never in Postgres. History is append-only and read with cursors. WebSockets vs SSE vs long-poll: choose by whether the server needs to push and the client needs to push. Trap: claiming exactly-once delivery. Say at-least-once plus idempotent consumption and move on.
6. Webhook delivery (outbound)
Shape: deliver events to third-party endpoints reliably, with retries and signing. Yours: Zoth's outbox relay (Day 60) and Traverse's inbound webhook HMAC verification over the raw body (Day 89). Deep dive: the outbox pattern is the correct answer for "do not lose the event" — write the event in the same transaction as the state change, relay it asynchronously, dedup on the consumer side. Delivery needs exponential backoff with jitter, a per-endpoint circuit breaker (a dead customer endpoint must not consume your whole worker pool), and a DLQ. Signatures must be computed over the raw body and include a timestamp to prevent replay. Trap: retrying forever. Retry with a budget, then park it where a human can see it.
7. Scheduler / cron at scale
Shape: run jobs on a schedule, exactly once per period, across many instances.
Yours: Zoth's ticker plus SKIP LOCKED claims; Traverse's node-cron jobs (which double-fire at two replicas — Day 50, a known and named flaw).
Deep dive: the naive answer (cron on every box) double-fires. The correct answers, in increasing sophistication: a DB advisory lock so only one instance runs; a claim table where workers SELECT ... FOR UPDATE SKIP LOCKED due rows; or a scheduler that materializes occurrences into a queue. Repeatable/delayed job support (which is exactly what BullMQ gives you, Day 59) is a build-vs-buy decision.
Trap: not mentioning idempotency. Even a correct scheduler will occasionally run a job twice; the job must tolerate it.
8. File uploads
Shape: accept large files, store them, serve them, don't melt the app server.
Yours: the recon streaming feature — write to a .partial file and atomically rename, with per-platform quotas (Day 82).
Deep dive: never proxy bytes through the app if you can hand out a presigned URL for direct-to-object-storage upload; the app's job is authorization and metadata, not throughput. Then: content-type sniffing (do not trust the client), size limits enforced at the edge, checksum verification, and lifecycle rules to move cold data to cheaper storage (Day 56). Serving: CDN in front, immutable cache headers for content-addressed keys.
Trap: forgetting the authorization step on the presigned URL — a presigned URL handed to the wrong user is a data breach.
9. Autocomplete / typeahead
Shape: prefix search returning ~10 results in under 100 ms. Yours: Zoth's pgvector knowledge base (semantic) — and the distinction from prefix search, which is lexical. Deep dive: this is a trie or a sorted-key index problem, not a vector problem. Options: a materialized prefix table in Postgres, a dedicated search engine with edge n-grams, or an in-memory trie. Ranking matters more than recall (popularity × recency × personalization). The killer constraint is freshness vs cost: rebuilding a trie is expensive, so you shard it and update incrementally. Debounce client-side, cache aggressively server-side. Trap: reaching for embeddings. Vector search is for meaning, not prefixes; the interviewer is checking whether you know the difference (Day 66).
10. Leaderboard
Shape: top-N by score, updating in real time, with a user's own rank.
Yours: Traverse's streaks and rings aggregations (Day 75) — and the HLL/count-min discussion from Day 71 for the "how many unique" cousin of this problem.
Deep dive: Redis sorted sets are purpose-built (ZADD, ZREVRANGE, ZRANK — the last one gives you a user's rank in O(log n), which is the part people forget). Exact global leaderboards need a single shard or periodic merge of per-shard top-K. Ties need a deterministic tiebreaker or ranks flicker. For "approximate top-K over huge cardinality", count-min sketch gives you heavy hitters in fixed memory (Day 71).
Trap: only answering top-10 and ignoring "what is my rank".
The meta-pattern across all ten
Look at what repeats: dedup keys, claim-based concurrency, cursor pagination, cache-aside for immutable data, intent-versus-attempt separation, and idempotent consumers. Those six ideas are the answer to roughly 80% of design prompts. In an interview, name them explicitly — "this is the same dedup-on-intent pattern as the notification system" — because naming the pattern proves you understand it as a pattern and not as trivia about one system.
Bulletproof your resume
Say these answers aloud before moving on
These are the follow-ups an interviewer will actually ask about your projects.
Design a notification service. Where do you start?
I separate intent from delivery. Intent is a row representing the product truth that a user should be told something, with a dedup key so the same event cannot produce two intents. Delivery is a per-channel state machine — pending, claimed, sent, failed, dead — with a lease-based claim so multiple workers can process the fan-out without double-sending, backoff with jitter on failure, and terminal statuses so nothing retries forever. Preferences, quiet hours, and per-channel caps are policy evaluated at intent time. Push is best-effort and the in-app record is the source of truth, because I cannot promise delivery on a channel I do not own. That is exactly what I built in Traverse, with a 120-second lease on the claim.
Fanout on write or fanout on read for a feed?
It depends on the read/write ratio and the follower distribution, so the honest answer is hybrid. Fanout on write precomputes each follower timeline — reads are a single indexed lookup, writes cost O(followers), and a celebrity with millions of followers makes writes pathological. Fanout on read queries the followee set at request time — writes are trivial but reads need an index shaped exactly like the query, and merging many lists gets expensive. The practical design fans out on write for normal accounts and merges on read for the top N high-follower accounts, with a cursor-based pagination contract and a bounded merge window. I would size N from the actual follower distribution, not from a guess.
Why are sorted sets the right answer for a leaderboard?
Because the three operations you need are all native and all logarithmic: ZADD to update a score, ZREVRANGE to read the top N, and ZRANK to get a specific member's rank. That last one is the operation people forget and it is the one that makes the feature work — a leaderboard that shows the top ten but cannot tell a user where they stand is not a leaderboard. Exact global rankings need either a single shard or periodic merges of per-shard top-K, ties need a deterministic tiebreaker so ranks do not flicker, and if cardinality is enormous and approximate heavy hitters are acceptable, a count-min sketch gives you top-K in fixed memory.