21. Patterns Playbook — What Recurs, and What to Reach For Synthesis Across All 20 Problems
A. Why This Section Exists
Ten problems that look unrelated on the surface — a URL shortener and a collaborative document editor share almost no domain vocabulary — turn out, underneath, to be built from a surprisingly small, repeating set of moves. Once you notice that Ticketmaster's overselling fix, Rate Limiter's atomic increment, and Uber's driver-assignment race are the same underlying problem wearing three different costumes, you stop needing to re-derive the answer from scratch every time it shows up — you just need to recognize which named pattern applies, and why.
This section is that catalog: each pattern names the recurring shape of the problem, the concrete mechanism reached for, where it actually appeared (with links back to the specific deep dive), and — just as important — when that mechanism is the wrong tool. Section 0 (Prerequisites) explained what each building block is; this section explains which building block to reach for, given a symptom, which is the actual skill being tested in a live interview.
B. Pattern 1 — The Check-Then-Act Race
The shape of the problem: a request reads some shared state ("is this available?"), decides what to do based on what it read, then writes a change based on that decision — and the gap between the read and the write is exactly wide enough for a second, concurrent request to read the same stale state and make the same decision, resulting in two writes that should never have both succeeded.
The fix, in every case: collapse the read-decide-write sequence into a single atomic operation, so there is no gap in which a second request can observe the same pre-decision state.
| Where It Appeared | What Was Racing | Mechanism Used |
|---|---|---|
| Rate Limiter (P2, Deep Dive N) | Two requests both reading "under the limit" before either increments the counter | A single atomic Redis operation (or a Lua script bundling check + increment) |
| Ride-Sharing / Uber (P5, Deep Dive P) | Two riders' requests both matching to the same available driver | An atomic conditional update (compare-and-swap) on the driver's assignment status |
| Ticketmaster (P8, Deep Dive M) | Two buyers both reserving the same seat | A single atomic conditional database update (UPDATE ... WHERE status='available'), checked via affected-row count |
| Notification System (P12) | Two requests both checking "not yet seen" for the same dedup key before either claims it | Redis's atomic SET key NX EX dedup_window — the check and the claim collapsed into one call |
| Distributed File Storage (P14) | Two devices both committing a new version of the same file | A version-vector compare-and-swap at commit time — the one instance in this document where the "losing" request isn't rejected, but resolved into a conflicted copy instead |
| Payment System (P15) | Two concurrent requests carrying the same idempotency key, both arriving before either has finished processing | A unique database constraint on (merchant_id, key), enforced as a single atomic insert-if-absent on the idempotency record itself |
When NOT to reach for an external distributed lock as the fix: if the entire check-then-act sequence can be expressed as one atomic database statement or one Redis command/Lua script, that's simpler and more robust than adding a separate lock service — reserve an external lock for when the protected operation genuinely needs to span systems a single atomic statement can't reach (Prerequisites E elaborates on this trade-off directly).
C. Pattern 2 — Keep Expensive Work Off the Hot Path
The shape of the problem: a user-facing request has one fast, essential part and one slow, non-essential part bundled together — and doing both synchronously means the user waits for the slow part even though it doesn't actually need to finish before a response can be returned.
The fix: do the essential part synchronously, hand the slow part off to an async queue and a background worker, and return to the user immediately. The user-visible latency becomes the fast part's latency only.
| Where It Appeared | The Fast, Essential Part | The Slow Part, Moved Off-Path |
|---|---|---|
| URL Shortener (P1) | Redirecting the user | Recording click analytics |
| News Feed (P3) | Accepting a new post | Fanning it out to every follower's feed |
| Video Streaming (P7) | Accepting an uploaded file | Transcoding it into multiple resolutions |
| Video Streaming (P7) | Serving a video playback request | Aggregating/incrementing the view count |
| Ticketmaster (P8) | Holding the seat | N/A — deliberately the one place this pattern is not applied; see the callout below |
| Notification System (P12) | Accepting a notification request | Actually delivering it through the push/SMS/email provider |
| Ad Click Aggregation (P16) | Accepting a click event into the ingestion log | Windowed aggregation, HyperLogLog rollups, and fraud filtering |
| Yelp (P17) | Serving a business's rating | Recomputing the aggregate rating — incrementally (an O(1) running-sum adjustment per new review) or on a short batch interval, rather than an O(n) rescan on every read |
The important exception, worth stating explicitly: Ticketmaster's payment charge is not moved off the hot path the way analytics or view-counting are — it's sequenced strictly after the seat hold succeeds, not deferred to an async worker, because the user is actively waiting for a definitive yes/no on their purchase. Payment System (P15) generalizes this same exception across its whole design: nothing on the ledger-write path is deferred either, for the identical reason. This pattern applies specifically to work whose outcome the user doesn't need to see immediately — not to every "slow" operation indiscriminately.
D. Pattern 3 — Metadata / Blob Store Split
The shape of the problem: an entity has a small amount of structured, frequently-queried metadata (owner, timestamp, status) alongside a large binary payload (an image, a video file, a page of HTML) — and storing both together forces every system that touches the metadata to also carry the weight of the payload.
The fix: store the metadata in a database (fast to query, cheap to replicate) and the actual bytes in dedicated blob/object storage (I in Prerequisites), with the metadata row holding a pointer/key into the blob store.
| Where It Appeared | The Metadata | The Blob |
|---|---|---|
| News Feed (P3) | Post record (author, timestamp, caption) | Attached photo/video |
| Chat System (P4) | Message record (sender, timestamp, conversation_id) | Attached file/image |
| Web Crawler (P6) | URL, crawl timestamp, content hash | The fetched page's raw content |
| Video Streaming (P7) | Video record (title, owner, status) | Raw upload and each transcoded resolution |
| Google Docs (P9) | Current revision number | Periodic full-document snapshot |
| Distributed File Storage (P14) | File record + ordered chunk-hash list + version vector | The actual chunk bytes, deduplicated across the whole chunk store — even across different users' files |
E. Pattern 4 — Idempotent Retries via Client-Generated Keys
The shape of the problem: a client sends a mutating request, the request actually succeeds server-side, but the client never receives confirmation (a timeout, a dropped connection) — and from the client's perspective this is indistinguishable from the request never having arrived, so a naive retry risks repeating a side effect that already happened.
The fix: the client attaches a stable, unique key to the request; the server remembers which keys it has already processed and, on a repeat, returns the original result instead of repeating the side effect.
| Where It Appeared | The Key | The Side Effect Being Protected |
|---|---|---|
| Chat System (P4) | client_msg_id | Sending the same message twice |
| Ticketmaster (P8) | Payment confirmation idempotency key | Double-charging a customer's card |
| Job Scheduler (P10) | Per-execution idempotency key | A job's side effect (e.g. sending an email) firing twice after an at-least-once delivery redelivers it |
| Notification System (P12) | Client-supplied dedup key at ingest + a second check at the worker layer | Sending the same push/SMS/email twice on a client retry |
| Payment System (P15) | Mandatory idempotency key at the API boundary | Double-charging a card on a client or network retry — the highest-stakes instance of this pattern in the document |
| Ad Click Aggregation (P16) | Idempotency key + Bloom-filter pre-filter | Counting the same click twice from a retried/duplicated event |
| Distributed Message Queue (P20) | Consumer-side idempotent processing, paired with broker-level at-least-once delivery | Reprocessing a message the broker redelivers after a consumer crash/rebalance — the capstone example: the pattern applied by the queue's own consumers, not just by systems that happen to use a queue |
Why this pattern is really an admission, not a workaround: every one of these systems delivers at-least-once, not exactly-once, because exactly-once delivery isn't achievable over an unreliable network (Prerequisites O). Idempotency isn't a patch over a bug — it's the correct, honest design response to that fact.
F. Pattern 5 — Push vs. Pull (Fan-Out Write vs. Fan-Out Read)
The shape of the problem: one piece of content needs to reach many downstream viewers/consumers — and there's a real choice between doing that work once, in advance, at write time (push / fan-out-on-write) versus doing it fresh, on demand, at read time (pull / fan-out-on-read).
| Approach | Best When | Where Used |
|---|---|---|
| Push (fan-out on write) | Read volume vastly exceeds write volume, and readers have a typical/bounded fan-out (most users' feed) | News Feed's default path (P3) — a new post is proactively written into each follower's feed |
| Pull (fan-out on read) | The write side has an extreme, atypical fan-out (a celebrity with millions of followers) where pushing to every follower at write time would be prohibitively expensive | News Feed's celebrity-account exception (P3) — merged into the reader's feed at read time instead |
The general lesson: push and pull aren't competing answers to the same problem — they're each correct for a different shape of fan-out, and a mature design (as News Feed's does) uses both simultaneously, choosing per-case rather than picking one globally.
G. Pattern 6 — Shard by the Natural Ownership Key
The shape of the problem: a dataset is too large or too write-heavy for one machine, and needs to be split (sharded, per Prerequisites C) across many — but the wrong shard key forces most requests to fan out across many shards instead of hitting just one.
The fix: pick the shard key that matches the boundary within which data is almost always accessed together — the entity that "owns" a self-contained slice of the problem.
| Where It Appeared | Shard Key | Why That Key |
|---|---|---|
| Rate Limiter (P2) | User/client ID (via consistent hashing) | A rate-limit decision only ever needs that one client's own counters |
| Ticketmaster (P8) | event_id | All seats, holds, and orders for one event are always accessed together; contains one viral on-sale's load to its own shard |
| Google Docs (P9) | doc_id | All operations for one document must be sequenced by a single authority to have any well-defined order at all |
| Job Scheduler (P10) | job_id | A job's schedule, executions, and dependency edges are always evaluated as a unit |
| Real-Time Leaderboard (P19) | Segment key (per-game/region/time-window) | Score updates and rank queries within one segment are always evaluated together; keeps one viral game's write volume off every other segment's shard |
| Search Autocomplete (P13) | Query prefix (first one to two characters) | The same ownership-boundary reasoning applied to a tree's own structural branches instead of a row's primary key — one shard owns everything rooted under "a"-"f," another "g"-"m," and so on |
| Payment System (P15) | merchant_id | All of one merchant's ledger transactions are always accessed together; keeps one merchant's volume from contending with another's |
| Distributed Message Queue (P20) | Partition count and key-hash, sized deliberately upfront | Partition count isn't casually resizable the way a stateless service's instance count is — getting the shard key and its stability right matters more here than almost anywhere else this pattern appears |
H. Pattern 7 — Connection Routing at Scale
The shape of the problem: a fleet of servers each hold a slice of long-lived, stateful connections (WebSockets, per Prerequisites K) — and a message meant for a specific connected user has to somehow find whichever specific server currently holds that user's connection, out of potentially thousands of servers.
The fix: a shared, fast lookup table (user_id → server_id) combined with a pub/sub broker layer — a sender's server looks up the owning server, then publishes to a channel that server is subscribed to, decoupling "who has this connection right now" from "how do I actually deliver to them."
| Where It Appeared | What's Being Routed |
|---|---|
| Chat System (P4, Deep Dive N) | A message, to whichever connection server holds the recipient's WebSocket |
| Google Docs (P9, Deep Dive S) | A document's operations, to whichever coordinating server currently owns that doc_id |
I. Pattern 8 — Leader Election for Singleton Responsibilities
The shape of the problem: some function must be performed by exactly one active instance at a time — zero active instances means the function silently stops happening; more than one active instance means duplicated, potentially harmful work.
The fix: lease-based leader election (Prerequisites F) — instances race for a TTL'd lock representing the leader role; the winner renews it periodically; if it stops renewing, another instance takes over automatically.
| Where It Appeared | The Singleton Responsibility |
|---|---|
| Job Scheduler (P10, Deep Dive M) | Deciding which jobs are due to trigger right now — the central, defining example of this pattern in the document |
Why this is distinct from Pattern 1 (the check-then-act race): Pattern 1 is about two requests racing over a piece of data; this pattern is about ensuring exactly one instance is even allowed to act at all for an entire category of decisions — the granularity is a whole role, not a single record.
J. Pattern 9 — Narrow Strong-Consistency Islands
The shape of the problem: a system is, by default, eventually consistent almost everywhere (because that's simpler, cheaper, and more available) — but one specific piece of state genuinely cannot tolerate staleness or ambiguity without producing an actual correctness failure a user will notice.
The fix: default to eventual consistency everywhere, and deliberately carve out the smallest possible piece of state that needs strong consistency, giving only that piece the more expensive guarantee.
| Where It Appeared | The Strong-Consistency Island | Everything Else, Eventually Consistent |
|---|---|---|
| URL Shortener (P1) | Short-code uniqueness at creation time | The redirect-serving path itself |
| Ride-Sharing / Uber (P5) | A single driver-rider assignment | Driver location updates, ETA estimates |
| Ticketmaster (P8) | A seat's hold/sold status — the most strongly-consistency-weighted example in the document | The browse/seat-map display |
| Job Scheduler (P10) | The leader lock itself | Execution history/status visibility |
| Payment System (P15) | The double-entry ledger write itself — the strongest-consistency requirement in the document | Notifications, async reconciliation jobs, webhook processing |
| Notification System (P12) | The dedup check itself — a single atomic operation against a store that guarantees a check-and-claim never races | Delivery status, retry bookkeeping |
Another instructive exception — Distributed Key-Value Store (P18): rather than carving out one fixed strong-consistency island, it exposes the N/R/W quorum knobs directly to the caller, so any key can be read/written at whatever point on the consistency/availability spectrum that specific use case needs — the island isn't fixed by the system, it's chosen per-request.
The instructive exception — Google Docs (P9): its consistency model isn't "eventual by default, with one strong island" — it targets strong eventual consistency for content itself (all replicas converge, guaranteed, regardless of arrival order) precisely because there's no single "seat" to protect; every concurrent edit must be preserved and reconciled, never rejected. Worth noticing this is a genuinely different shape than Pattern 9's more common form, not a variation of it.
A third distinct shape — Distributed File Storage (P14): not a strong-consistency island (there's no single scarce resource being contended for — no seat, no leader lock) and not quite Google Docs's strong eventual consistency either — it's offline-tolerant, with conflicts surfaced to the user via a conflicted copy rather than silently merged or silently discarded. Worth keeping this as a third named shape rather than folding it into either of the other two.
K. Pattern 10 — Reusing Rate Limiting as a Cross-Cutting Concern
The shape of the problem: a system needs to bound how fast a given actor can perform some action — not as the main subject of the design, but as a supporting concern layered on top of something else entirely.
The fix: the token-bucket/sliding-window mechanisms designed once, in full, for Rate Limiter (P2) get reused directly wherever this need resurfaces elsewhere, rather than being reinvented per-problem.
| Where It Appeared | What's Being Bounded |
|---|---|
| Ticketmaster (P8, Deep Dive S) | Reservation attempts per user/IP, as part of bot/scalper mitigation |
| Notification System (P12, Deep Dive Q) | Notifications per user per channel, to prevent notification storms; batched into a digest above the limit rather than just dropped |
| Search Autocomplete (P13) | Queries per client, to bound the damage a malicious or buggy client hammering a rare, expensive prefix can do to the shared origin tier |
The lesson: recognizing "this is just rate limiting again" and pointing back to an already-solved problem, rather than re-deriving a bespoke throttling mechanism from scratch, is itself a signal of strong pattern recognition in an interview.
L. Pattern 11 — Consistent Hashing for Elastic Ownership
The shape of the problem: a fleet of servers each "own" some slice of a much larger keyspace (shards, connections, documents) — and the fleet needs to grow or shrink without triggering a massive, disruptive reshuffling of ownership every time.
The fix: consistent hashing (Prerequisites J) — map both servers and keys onto a ring, so adding or removing a server only reassigns the small slice of keys immediately adjacent to it.
| Where It Appeared | What's Being Elastically Assigned |
|---|---|
| Rate Limiter (P2) | Which shard owns a given client's rate-limit counters |
| Chat System (P4) | Which connection server a user's session lands on (in combination with Pattern 7's routing table) |
| Google Docs (P9, Deep Dive S) | Which coordinating server owns a given active document |
| Distributed Cache (P11, Deep Dive N) | Which cache shard owns a given key — the ring is the primary partitioning mechanism for the whole design, not an incidental detail |
| Distributed Key-Value Store (P18, Deep Dive M) | Which node (and, via the preference list, which replica set) owns a given key — the deepest treatment of this pattern in the document, including virtual-node count trade-offs |
| Distributed File Storage (P14) | Which chunk-store node owns a given chunk — an unusually clean case, since content-hash keys are already uniformly, pseudo-randomly distributed, with no hot-range concerns to design around |
M. Master "What to Use When" Decision Table
A condensed, symptom-first index into everything above — read down the left column for whatever problem you're facing, and land on the pattern and mechanism to reach for.
| Symptom / Question | Reach For | Pattern |
|---|---|---|
| "Two requests could both act on the same stale read" | Atomic conditional update / single Redis command or Lua script | B — Check-Then-Act Race |
| "This request has a slow part the user doesn't need to wait for" | Async queue + background worker | C — Hot Path Offloading |
| "This entity has a small record plus a large binary payload" | Metadata in DB, bytes in blob/object storage | D — Metadata/Blob Split |
| "A retried request might repeat a real-world side effect" | Client-generated idempotency key, checked server-side | E — Idempotent Retries |
| "One write needs to reach a huge, variable number of readers" | Push (fan-out on write) normally; pull (fan-out on read) for extreme outlier fan-out | F — Push vs. Pull |
| "This dataset needs to be split across machines" | Shard by the key that matches the natural access-together boundary | G — Shard by Ownership Key |
| "A message needs to reach a specific user's persistent connection" | Shared session store (user_id → server_id) + pub/sub broker | H — Connection Routing |
| "Exactly one instance must perform this function, never zero, never two" | Lease-based leader election | I — Leader Election |
| "Most of this can be eventually consistent, but not this one specific field" | Default to eventual consistency; isolate one narrow strong-consistency island | J — Strong-Consistency Islands |
| "We need to throttle some actor's request rate, as a side concern" | Reuse token bucket / sliding window, don't reinvent | K — Rate Limiting Reuse |
| "A fleet of servers needs to grow/shrink without mass reassignment" | Consistent hashing ring | L — Consistent Hashing |
N. How to Use This in an Actual Interview
The value of this catalog isn't reciting pattern names — it's the speed and confidence it gives you when a new, unfamiliar-sounding prompt turns out to contain a familiar shape. When an interviewer describes a new problem, listen for which symptom in Section M's table is actually present, and reach for that mechanism directly rather than re-deriving it from first principles under time pressure. Just as importantly, listen for when a pattern doesn't apply — Section C's callout on Ticketmaster's payment flow is a reminder that these are judgment tools, not checklists to apply mechanically regardless of context. The strongest signal you can give an interviewer is naming the pattern, explaining why it fits this specific case, and — just as tellingly — explaining why a superficially similar pattern would be the wrong fit here.