@daowolf/system-design-quest 0.0.0-stage → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +64 -2
- package/content/LICENSE.md +8 -0
- package/dist/chapters.json +4154 -0
- package/dist/element.js +4 -0
- package/dist/element.js.map +7 -0
- package/dist/style.css +792 -0
- package/package.json +62 -3
|
@@ -0,0 +1,4154 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"built": "2026-10-28",
|
|
4
|
+
"chapters": [
|
|
5
|
+
{
|
|
6
|
+
"chapter": 1,
|
|
7
|
+
"slug": "method",
|
|
8
|
+
"title": "The method",
|
|
9
|
+
"emoji": "🧭",
|
|
10
|
+
"pillar": "The interview",
|
|
11
|
+
"blurb": "Run the round in the right order, decide instead of asking, and land a whole design before going deep.",
|
|
12
|
+
"boss": "m-boss",
|
|
13
|
+
"challenges": [
|
|
14
|
+
{
|
|
15
|
+
"id": "m-01",
|
|
16
|
+
"type": "order",
|
|
17
|
+
"bloom": "remember",
|
|
18
|
+
"prompt": "Put the steps of a system design round in order.",
|
|
19
|
+
"items": [
|
|
20
|
+
"Functional requirements",
|
|
21
|
+
"Non-functional requirements",
|
|
22
|
+
"Core entities",
|
|
23
|
+
"API",
|
|
24
|
+
"High-level design",
|
|
25
|
+
"Deep dives"
|
|
26
|
+
],
|
|
27
|
+
"explain": "Requirements first, because every later box has to serve one. The high-level design meets the functional requirements; the deep dives meet the non-functional ones. Say it as your plan in the first minute.",
|
|
28
|
+
"source": "https://www.hellointerview.com/learn/system-design/in-a-hurry/delivery"
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"id": "m-02",
|
|
32
|
+
"type": "multi",
|
|
33
|
+
"bloom": "understand",
|
|
34
|
+
"prompt": "Which of these are non-functional requirements?",
|
|
35
|
+
"options": [
|
|
36
|
+
"Users can upload a photo",
|
|
37
|
+
"Reads return in under 200 ms for 99% of requests",
|
|
38
|
+
"The system stays up if one data centre goes down",
|
|
39
|
+
"Users can follow other users",
|
|
40
|
+
"A payment is never applied twice",
|
|
41
|
+
"Admins can delete posts"
|
|
42
|
+
],
|
|
43
|
+
"correct": [
|
|
44
|
+
1,
|
|
45
|
+
2,
|
|
46
|
+
4
|
|
47
|
+
],
|
|
48
|
+
"explain": "Functional requirements say what users can do (\"users should be able to…\"). Non-functional ones say how well the system must do it (\"the system should…\"), and they carry numbers. They decide your deep dives.",
|
|
49
|
+
"source": "https://www.hellointerview.com/learn/system-design/in-a-hurry/delivery"
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"id": "m-03",
|
|
53
|
+
"type": "duel",
|
|
54
|
+
"bloom": "evaluate",
|
|
55
|
+
"prompt": "The interviewer says \"up to you\" about whether refunds are in scope. What do you say?",
|
|
56
|
+
"choices": [
|
|
57
|
+
"\"Do you want refunds in scope?\"",
|
|
58
|
+
"\"I'll assume refunds are out. Tell me if that's wrong.\"",
|
|
59
|
+
"\"Let's include everything, to be safe.\""
|
|
60
|
+
],
|
|
61
|
+
"correct": 1,
|
|
62
|
+
"reasons": [
|
|
63
|
+
"It shows judgement and keeps the time for the hard parts",
|
|
64
|
+
"Interviewers mark you on how many questions you ask",
|
|
65
|
+
"It means you can never be wrong"
|
|
66
|
+
],
|
|
67
|
+
"reason": 0,
|
|
68
|
+
"costs": [
|
|
69
|
+
"If they did want refunds, you adjust in one sentence",
|
|
70
|
+
"Nothing; assumptions are free",
|
|
71
|
+
"You lose the marks for problem navigation"
|
|
72
|
+
],
|
|
73
|
+
"cost": 0,
|
|
74
|
+
"explain": "Asking hands the decision back to them; deciding and checking shows you can lead. The cost is tiny, because \"tell me if that's wrong\" invites a correction. Say it as a decision, then check.",
|
|
75
|
+
"source": "https://www.hellointerview.com/blog/staff-level-system-design"
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
"id": "m-04",
|
|
79
|
+
"type": "recall",
|
|
80
|
+
"bloom": "remember",
|
|
81
|
+
"prompt": "The interviewer has just given you the problem. What's your first move?",
|
|
82
|
+
"answer": "Write it down in their words and read it back: \"So we're building X for Y. Have I got that right?\"",
|
|
83
|
+
"explain": "Reading it back catches a misheard problem before you spend twenty minutes on it, and it shows you listen. It takes ten seconds.",
|
|
84
|
+
"source": "https://www.hellointerview.com/learn/system-design/in-a-hurry/delivery"
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
"id": "m-05",
|
|
88
|
+
"type": "multi",
|
|
89
|
+
"bloom": "analyse",
|
|
90
|
+
"prompt": "Fifty minutes isn't enough to go deep on everything. Which of these usually earn deep-dive time?",
|
|
91
|
+
"options": [
|
|
92
|
+
"Money moving",
|
|
93
|
+
"Work that runs on a schedule",
|
|
94
|
+
"Calls to an outside partner",
|
|
95
|
+
"Very high read or write volume",
|
|
96
|
+
"A static help page",
|
|
97
|
+
"A profile settings form",
|
|
98
|
+
"Changing a display name"
|
|
99
|
+
],
|
|
100
|
+
"correct": [
|
|
101
|
+
0,
|
|
102
|
+
1,
|
|
103
|
+
2,
|
|
104
|
+
3
|
|
105
|
+
],
|
|
106
|
+
"explain": "Go deep where things break or cost money: money, timing, partners and scale. The rest take the simple path, one sentence each. Say \"I'll go deep on X, Y and Z; the rest take the simple path.\"",
|
|
107
|
+
"source": "https://www.hellointerview.com/blog/staff-level-system-design"
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
"id": "m-06",
|
|
111
|
+
"type": "write",
|
|
112
|
+
"bloom": "apply",
|
|
113
|
+
"prompt": "Write the non-functional requirements for a URL shortener, with numbers. Don't forget the busiest time.",
|
|
114
|
+
"key_points": [
|
|
115
|
+
"A scale figure (redirects or new links a day)",
|
|
116
|
+
"A peak figure, not just the average",
|
|
117
|
+
"A latency target for a redirect",
|
|
118
|
+
"Availability: redirects must stay up",
|
|
119
|
+
"Reads far outnumber writes"
|
|
120
|
+
],
|
|
121
|
+
"model": "About 100 million redirects a day, ten times the average rate at the peak; a redirect in under 100 ms for 99% of requests; redirects available 99.99% of the time; reads outnumber new links about 100 to 1; a short link never points at the wrong place.",
|
|
122
|
+
"explain": "Numbers turn \"fast and reliable\" into decisions. The peak, not the average, sizes the system, and the read-to-write ratio tells you a cache will pay off. Say \"the system should…\" and put a number on each.",
|
|
123
|
+
"source": "https://www.hellointerview.com/learn/system-design/problem-breakdowns/bitly"
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
"id": "m-07",
|
|
127
|
+
"type": "recall",
|
|
128
|
+
"bloom": "understand",
|
|
129
|
+
"prompt": "Why land a simple design end to end before going deep on any part?",
|
|
130
|
+
"answer": "A working whole proves every requirement has a path and lets the interviewer steer the deep dive. Not reaching a working system is a common way to fail.",
|
|
131
|
+
"explain": "Depth on one box with gaps elsewhere reads as poor navigation. Say \"Let me land a simple version end to end first\", then deepen where the requirements push.",
|
|
132
|
+
"source": "https://www.hellointerview.com/learn/system-design/in-a-hurry/delivery"
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
"id": "m-08",
|
|
136
|
+
"type": "duel",
|
|
137
|
+
"bloom": "evaluate",
|
|
138
|
+
"prompt": "Ten minutes into the high-level design, you can see reads will be heavy. Nobody has asked about performance yet. Best move?",
|
|
139
|
+
"choices": [
|
|
140
|
+
"Design the cache in full now",
|
|
141
|
+
"Say where a cache would go, note it, and keep landing the design",
|
|
142
|
+
"Leave caching out; it's an optimisation"
|
|
143
|
+
],
|
|
144
|
+
"correct": 1,
|
|
145
|
+
"reasons": [
|
|
146
|
+
"It shows you see the bottleneck without derailing the whole design",
|
|
147
|
+
"Caches are always marked down in interviews",
|
|
148
|
+
"Interviewers never ask about performance"
|
|
149
|
+
],
|
|
150
|
+
"reason": 0,
|
|
151
|
+
"costs": [
|
|
152
|
+
"You owe it a proper look in the deep dive",
|
|
153
|
+
"The design gets slower",
|
|
154
|
+
"None"
|
|
155
|
+
],
|
|
156
|
+
"cost": 0,
|
|
157
|
+
"explain": "Naming it shows judgement; designing it now spends deep-dive time before the whole exists. Say \"A cache would go here; I'll come back to it\", and do.",
|
|
158
|
+
"source": "https://www.hellointerview.com/learn/system-design/in-a-hurry/delivery"
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
"id": "m-09",
|
|
162
|
+
"type": "lightning",
|
|
163
|
+
"bloom": "remember",
|
|
164
|
+
"prompt": "When this happens, say what? 90 seconds.",
|
|
165
|
+
"pairs": [
|
|
166
|
+
[
|
|
167
|
+
"They say \"you decide\"",
|
|
168
|
+
"\"I'll assume X. Tell me if that's wrong.\""
|
|
169
|
+
],
|
|
170
|
+
[
|
|
171
|
+
"You're stuck or going round in circles",
|
|
172
|
+
"\"Let me park that and land the rest.\""
|
|
173
|
+
],
|
|
174
|
+
[
|
|
175
|
+
"They push back on a choice",
|
|
176
|
+
"\"Good challenge.\" Weigh it, decide, move on"
|
|
177
|
+
],
|
|
178
|
+
[
|
|
179
|
+
"It's time to draw",
|
|
180
|
+
"\"Let me land a simple version end to end first.\""
|
|
181
|
+
],
|
|
182
|
+
[
|
|
183
|
+
"A tool you haven't run in production",
|
|
184
|
+
"\"I haven't run X in production. What I'd need from it is Y.\""
|
|
185
|
+
],
|
|
186
|
+
[
|
|
187
|
+
"The wrap-up starts",
|
|
188
|
+
"\"Three things I'd do next.\""
|
|
189
|
+
],
|
|
190
|
+
[
|
|
191
|
+
"You need a moment to think",
|
|
192
|
+
"\"Let me think for ten seconds.\""
|
|
193
|
+
],
|
|
194
|
+
[
|
|
195
|
+
"You're about to add a box",
|
|
196
|
+
"Say what it solves and what it costs"
|
|
197
|
+
]
|
|
198
|
+
],
|
|
199
|
+
"explain": "Pre-loaded lines stop silence and stop you handing decisions back. Each one buys time and keeps you leading.",
|
|
200
|
+
"source": "https://www.hellointerview.com/blog/staff-level-system-design"
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
"id": "m-boss",
|
|
204
|
+
"type": "sprint",
|
|
205
|
+
"bloom": "create",
|
|
206
|
+
"prompt": "Requirements for a Pastebin (people paste text and share a link). Out loud, against the clock: read it back, who uses it and what they do, your three hard parts, how busy (with the peak), what can't break, what's out.",
|
|
207
|
+
"minutes": 10,
|
|
208
|
+
"checks": [
|
|
209
|
+
"Read the problem back in one line",
|
|
210
|
+
"Named each type of user and what they do",
|
|
211
|
+
"Picked my own three hard parts",
|
|
212
|
+
"Asked for the scale and the peak",
|
|
213
|
+
"Said \"I'll assume…\" instead of asking \"do you want…?\"",
|
|
214
|
+
"Said what's out of scope",
|
|
215
|
+
"Finished inside the time"
|
|
216
|
+
],
|
|
217
|
+
"explain": "This is the shape of every round's first quarter. If one check missed, that's the line to rehearse before the next round.",
|
|
218
|
+
"source": "https://github.com/donnemartin/system-design-primer"
|
|
219
|
+
}
|
|
220
|
+
]
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
"chapter": 2,
|
|
224
|
+
"slug": "requirements",
|
|
225
|
+
"title": "Requirements and numbers",
|
|
226
|
+
"emoji": "🟣",
|
|
227
|
+
"pillar": "Core concepts",
|
|
228
|
+
"blurb": "Turn a vague brief into numbers. Functional versus non-functional, sizing for the peak, and the yardsticks you do the sums with.",
|
|
229
|
+
"boss": "r-boss",
|
|
230
|
+
"challenges": [
|
|
231
|
+
{
|
|
232
|
+
"id": "r-01",
|
|
233
|
+
"type": "multi",
|
|
234
|
+
"bloom": "analyse",
|
|
235
|
+
"prompt": "A cloud documents app. Which of these non-functional requirements could you actually test as written?",
|
|
236
|
+
"options": [
|
|
237
|
+
"The app should be fast",
|
|
238
|
+
"Opening a document takes under 300 ms (milliseconds) for 99% of requests",
|
|
239
|
+
"The system should be highly available",
|
|
240
|
+
"Up 99.9% of the time, so at most about 9 hours down a year",
|
|
241
|
+
"A saved edit survives the loss of a whole data centre",
|
|
242
|
+
"Other readers see an edit within 2 seconds",
|
|
243
|
+
"It should scale well",
|
|
244
|
+
"Users can share a document with a link"
|
|
245
|
+
],
|
|
246
|
+
"correct": [
|
|
247
|
+
1,
|
|
248
|
+
3,
|
|
249
|
+
4,
|
|
250
|
+
5
|
|
251
|
+
],
|
|
252
|
+
"explain": "A figure can be checked and drives a decision; \"fast\", \"highly available\" and \"scales well\" can't. Sharing a link is a functional requirement, not a quality. Fix: give each quality a number (a latency for 99% of requests, nines of availability, what loss is tolerable, how stale a read may be). Say \"the system should open a document in under 300 ms for 99% of requests\".",
|
|
253
|
+
"source": "https://www.hellointerview.com/learn/system-design/in-a-hurry/delivery"
|
|
254
|
+
},
|
|
255
|
+
{
|
|
256
|
+
"id": "r-02",
|
|
257
|
+
"type": "recall",
|
|
258
|
+
"bloom": "understand",
|
|
259
|
+
"prompt": "Why size a system for its busiest second rather than the daily average?",
|
|
260
|
+
"answer": "Traffic bunches into busy hours and spikes. A system built for the average falls over exactly when most people use it. Work out the average, multiply by a peak factor, and say the factor out loud.",
|
|
261
|
+
"explain": "An average hides the evening rush, a launch or a post that takes off. Interview sums use a multiplier: this game uses 10, and worked examples range from about 2 to 100. Say \"about 2,000 a second on average; I'll call the peak 20,000 and design for that\".",
|
|
262
|
+
"source": "https://www.hellointerview.com/learn/system-design/problem-breakdowns/bitly"
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
"id": "r-03",
|
|
266
|
+
"type": "estimate",
|
|
267
|
+
"bloom": "apply",
|
|
268
|
+
"prompt": "A photo-sharing app. Size its busiest second. Use about 100,000 seconds in a day and a peak of about 10 times the average.",
|
|
269
|
+
"generator": {
|
|
270
|
+
"kind": "peak_rps",
|
|
271
|
+
"params": {
|
|
272
|
+
"users": [
|
|
273
|
+
1000000,
|
|
274
|
+
50000000
|
|
275
|
+
],
|
|
276
|
+
"actions": [
|
|
277
|
+
5,
|
|
278
|
+
50
|
|
279
|
+
]
|
|
280
|
+
}
|
|
281
|
+
},
|
|
282
|
+
"explain": "A day has 86,400 seconds. Rounding up to 100,000 turns the division into dropping five zeros, and the small error vanishes inside the peak factor. Then multiply by 10. Say the working aloud, because the method earns the marks, not the exact digits.",
|
|
283
|
+
"source": "https://www.hellointerview.com/learn/system-design/problem-breakdowns/top-k"
|
|
284
|
+
},
|
|
285
|
+
{
|
|
286
|
+
"id": "r-04",
|
|
287
|
+
"type": "duel",
|
|
288
|
+
"bloom": "evaluate",
|
|
289
|
+
"prompt": "An online shop's catalogue. Shoppers view product pages about 1,000 times for every edit the staff make. The database is starting to struggle. What do you add first?",
|
|
290
|
+
"choices": [
|
|
291
|
+
"Split the database across several machines (sharding)",
|
|
292
|
+
"A cache in front of the database for product pages",
|
|
293
|
+
"A queue to buffer the writes",
|
|
294
|
+
"A bigger machine that only takes writes"
|
|
295
|
+
],
|
|
296
|
+
"correct": 1,
|
|
297
|
+
"reasons": [
|
|
298
|
+
"Nearly all the load is reads, and a cache read takes about 1 ms, a query 20-50 ms",
|
|
299
|
+
"A cache makes writes faster",
|
|
300
|
+
"A shop never needs sharding"
|
|
301
|
+
],
|
|
302
|
+
"reason": 0,
|
|
303
|
+
"costs": [
|
|
304
|
+
"A product page can be slightly out of date until its cached copy is refreshed",
|
|
305
|
+
"Every read now hits the database twice",
|
|
306
|
+
"Nothing, because caches are free"
|
|
307
|
+
],
|
|
308
|
+
"cost": 0,
|
|
309
|
+
"explain": "The read-to-write ratio tells you where to spend. At 1,000 reads per write the writes are tiny, so sharding solves a problem you don't have. A cache takes most reads off the database. Say \"Reads dominate, so a cache first. I'll accept a few seconds of staleness and refresh a page's cached copy when it's edited.\"",
|
|
310
|
+
"source": "https://www.hellointerview.com/learn/system-design/in-a-hurry/core-concepts"
|
|
311
|
+
},
|
|
312
|
+
{
|
|
313
|
+
"id": "r-05",
|
|
314
|
+
"type": "estimate",
|
|
315
|
+
"bloom": "apply",
|
|
316
|
+
"prompt": "An invoicing service keeps a file copy of every invoice it issues. How much storage does a year of new invoices need? Each step from KB to MB to GB to TB (kilobytes up to terabytes) is about 1,000 times the last.",
|
|
317
|
+
"generator": {
|
|
318
|
+
"kind": "storage_year",
|
|
319
|
+
"params": {
|
|
320
|
+
"items": [
|
|
321
|
+
10000,
|
|
322
|
+
1000000
|
|
323
|
+
],
|
|
324
|
+
"kb": [
|
|
325
|
+
50,
|
|
326
|
+
500
|
|
327
|
+
]
|
|
328
|
+
}
|
|
329
|
+
},
|
|
330
|
+
"explain": "Size a day (items times size), times 365 for a year, then step the units up: a thousand KB is an MB, a thousand MB a GB, a thousand GB a TB. Name the unit at every step so a slip shows. Files like these belong in object storage, with only their details in the database; a single database copes comfortably with a few TB.",
|
|
331
|
+
"source": "https://www.hellointerview.com/learn/system-design/in-a-hurry/core-concepts"
|
|
332
|
+
},
|
|
333
|
+
{
|
|
334
|
+
"id": "r-06",
|
|
335
|
+
"type": "write",
|
|
336
|
+
"bloom": "apply",
|
|
337
|
+
"prompt": "A podcast hosting site: creators upload episodes, listeners stream them. Rewrite this brief with numbers: \"It must be fast, always up, never lose an upload and cope with growth.\"",
|
|
338
|
+
"key_points": [
|
|
339
|
+
"Latency with a percentile, e.g. an episode starts playing within 1 second for 99% of plays",
|
|
340
|
+
"Availability as nines, with what that allows, e.g. 99.9% is about 9 hours down a year",
|
|
341
|
+
"Durability, e.g. once an upload is confirmed it survives losing a data centre",
|
|
342
|
+
"Scale with a peak: plays a day, and the busiest second at about 10 times the average",
|
|
343
|
+
"The read-to-write ratio, e.g. plays far outnumber uploads"
|
|
344
|
+
],
|
|
345
|
+
"model": "Episodes start within 1 second for 99% of plays. Listening is up 99.9% of the time (about 9 hours down a year); uploads can tolerate a little more. Once an upload is confirmed it survives losing a data centre. 10 million plays a day, peaking at about 10 times the average. Around 10,000 plays per upload, so reads dominate.",
|
|
346
|
+
"explain": "Each vague word hides a decision. A number for each one tells the interviewer what you're designing for and gives every deep dive a target. Say \"the system should…\", then the figure, then which part it applies to, because listening may need more nines than uploading.",
|
|
347
|
+
"source": "https://www.hellointerview.com/learn/system-design/in-a-hurry/delivery"
|
|
348
|
+
},
|
|
349
|
+
{
|
|
350
|
+
"id": "r-07",
|
|
351
|
+
"type": "multi",
|
|
352
|
+
"bloom": "analyse",
|
|
353
|
+
"prompt": "A page must load in under 250 ms. The app server makes its database reads one after another. Which set-ups fit inside that budget?",
|
|
354
|
+
"options": [
|
|
355
|
+
"Four queries to a database in the same data centre",
|
|
356
|
+
"Four queries to a database in another region",
|
|
357
|
+
"Four reads that all hit a cache in the same data centre",
|
|
358
|
+
"One combined query to a database in another region",
|
|
359
|
+
"Four calls to a service in another region, each running one query"
|
|
360
|
+
],
|
|
361
|
+
"correct": [
|
|
362
|
+
0,
|
|
363
|
+
2,
|
|
364
|
+
3
|
|
365
|
+
],
|
|
366
|
+
"explain": "Round trips decide it. Inside one data centre a hop costs under 1-2 ms; to another region it costs 50-150 ms, before the query's own 20-50 ms. Four in a row across regions blow the budget; one does not. Fix: keep chatty calls in one region, batch them, or cache. Ask \"how many round trips, and how far?\"",
|
|
367
|
+
"source": "https://www.hellointerview.com/learn/system-design/deep-dives/numbers-to-know"
|
|
368
|
+
},
|
|
369
|
+
{
|
|
370
|
+
"id": "r-08",
|
|
371
|
+
"type": "estimate",
|
|
372
|
+
"bloom": "analyse",
|
|
373
|
+
"prompt": "A web analytics product records every click on its customers' websites. Work out the peak writes a second, then decide whether one database copes or the data needs splitting.",
|
|
374
|
+
"generator": {
|
|
375
|
+
"kind": "shard_or_not",
|
|
376
|
+
"params": {
|
|
377
|
+
"users": [
|
|
378
|
+
100000,
|
|
379
|
+
50000000
|
|
380
|
+
],
|
|
381
|
+
"writes": [
|
|
382
|
+
10,
|
|
383
|
+
200
|
|
384
|
+
]
|
|
385
|
+
}
|
|
386
|
+
},
|
|
387
|
+
"explain": "One well-tuned SQL (Structured Query Language) database handles up to about 50,000 simple transactions a second, but writes strain it first. Past about 10,000 writes a second, consider splitting the data across machines (sharding). Below that, say \"one database plus a standby copy; no sharding yet\". Sharding early buys complexity you don't need.",
|
|
388
|
+
"source": "https://www.hellointerview.com/learn/system-design/in-a-hurry/core-concepts"
|
|
389
|
+
},
|
|
390
|
+
{
|
|
391
|
+
"id": "r-09",
|
|
392
|
+
"type": "order",
|
|
393
|
+
"bloom": "understand",
|
|
394
|
+
"prompt": "Put these in order, fastest first.",
|
|
395
|
+
"items": [
|
|
396
|
+
"Reading a value from the app's own memory",
|
|
397
|
+
"Reading a small block from a local solid-state drive (SSD)",
|
|
398
|
+
"Reading from a cache server in the same data centre",
|
|
399
|
+
"Running a typical database query",
|
|
400
|
+
"A round trip to another region"
|
|
401
|
+
],
|
|
402
|
+
"explain": "Memory takes nanoseconds, an SSD microseconds, a cache about 1 ms, a database query 20-50 ms and another region 50-150 ms. That ladder is why a cache pays off and why chatty calls across regions hurt. Say the number whenever you add a box or a hop.",
|
|
403
|
+
"source": "https://www.hellointerview.com/learn/system-design/in-a-hurry/core-concepts"
|
|
404
|
+
},
|
|
405
|
+
{
|
|
406
|
+
"id": "r-boss",
|
|
407
|
+
"type": "lightning",
|
|
408
|
+
"bloom": "remember",
|
|
409
|
+
"prompt": "The yardsticks. You hear the phrase; give the number or the rule. 90 seconds.",
|
|
410
|
+
"seconds": 90,
|
|
411
|
+
"pairs": [
|
|
412
|
+
[
|
|
413
|
+
"Seconds in a day, for interview sums",
|
|
414
|
+
"About 100,000"
|
|
415
|
+
],
|
|
416
|
+
[
|
|
417
|
+
"The peak, compared with the average",
|
|
418
|
+
"About 10 times the average"
|
|
419
|
+
],
|
|
420
|
+
[
|
|
421
|
+
"One step from KB to MB, or from GB to TB",
|
|
422
|
+
"Times 1,000"
|
|
423
|
+
],
|
|
424
|
+
[
|
|
425
|
+
"What one well-tuned SQL database can take",
|
|
426
|
+
"Up to about 50,000 transactions a second"
|
|
427
|
+
],
|
|
428
|
+
[
|
|
429
|
+
"When to think about splitting the database",
|
|
430
|
+
"Past about 10,000 writes a second"
|
|
431
|
+
],
|
|
432
|
+
[
|
|
433
|
+
"A read from a cache",
|
|
434
|
+
"About 1 ms"
|
|
435
|
+
],
|
|
436
|
+
[
|
|
437
|
+
"A typical database query",
|
|
438
|
+
"20-50 ms"
|
|
439
|
+
],
|
|
440
|
+
[
|
|
441
|
+
"A call inside one data centre",
|
|
442
|
+
"Under 1-2 ms"
|
|
443
|
+
],
|
|
444
|
+
[
|
|
445
|
+
"A round trip to another region",
|
|
446
|
+
"50-150 ms"
|
|
447
|
+
],
|
|
448
|
+
[
|
|
449
|
+
"Reads far outnumber writes",
|
|
450
|
+
"Cache first, then read copies"
|
|
451
|
+
]
|
|
452
|
+
],
|
|
453
|
+
"explain": "These numbers turn hand-waving into decisions in seconds. If one was slow, write it on a card and say it aloud before the next round.",
|
|
454
|
+
"source": "https://www.hellointerview.com/learn/system-design/in-a-hurry/core-concepts"
|
|
455
|
+
}
|
|
456
|
+
]
|
|
457
|
+
},
|
|
458
|
+
{
|
|
459
|
+
"chapter": 3,
|
|
460
|
+
"slug": "apis",
|
|
461
|
+
"title": "APIs and live updates",
|
|
462
|
+
"emoji": "🔌",
|
|
463
|
+
"pillar": "Core concepts",
|
|
464
|
+
"blurb": "APIs (application programming interfaces) over HTTP (Hypertext Transfer Protocol): pick a style, make retries safe, page through lists, stream live updates.",
|
|
465
|
+
"boss": "a-boss",
|
|
466
|
+
"challenges": [
|
|
467
|
+
{
|
|
468
|
+
"id": "a-01",
|
|
469
|
+
"type": "duel",
|
|
470
|
+
"bloom": "evaluate",
|
|
471
|
+
"prompt": "Two of your own backend services call each other thousands of times a second, and no browser ever uses this path. Which style fits: REST (representational state transfer), GraphQL, or gRPC (a remote procedure call, or RPC, framework)?",
|
|
472
|
+
"choices": [
|
|
473
|
+
"REST, with JSON (JavaScript Object Notation) bodies",
|
|
474
|
+
"GraphQL",
|
|
475
|
+
"gRPC, with Protocol Buffers over HTTP/2"
|
|
476
|
+
],
|
|
477
|
+
"correct": 2,
|
|
478
|
+
"reasons": [
|
|
479
|
+
"Compact binary messages and generated, typed clients suit busy internal calls",
|
|
480
|
+
"It is the only style that works between two services",
|
|
481
|
+
"Browsers support it better than REST"
|
|
482
|
+
],
|
|
483
|
+
"reason": 0,
|
|
484
|
+
"costs": [
|
|
485
|
+
"Browsers can't call it directly, and binary messages are harder to eyeball",
|
|
486
|
+
"It can't stream results",
|
|
487
|
+
"Its messages are bigger than JSON"
|
|
488
|
+
],
|
|
489
|
+
"cost": 0,
|
|
490
|
+
"explain": "gRPC sends Protocol Buffers, a compact binary format, over HTTP/2 and generates typed client code, so busy internal calls get cheaper and safer. REST stays the default for anything public or called from a browser. Say \"REST at the edge, gRPC between services\" and move on: it's worth a mention, not a deep dive.",
|
|
491
|
+
"source": "https://www.hellointerview.com/learn/system-design/core-concepts/networking-essentials"
|
|
492
|
+
},
|
|
493
|
+
{
|
|
494
|
+
"id": "a-02",
|
|
495
|
+
"type": "multi",
|
|
496
|
+
"bloom": "analyse",
|
|
497
|
+
"prompt": "Which of these point towards GraphQL rather than REST?",
|
|
498
|
+
"options": [
|
|
499
|
+
"Web, mobile and watch apps each need different fields from the same data",
|
|
500
|
+
"One screen would otherwise take five round trips to put together",
|
|
501
|
+
"Responses must be cached by URL (web address) in shared caches",
|
|
502
|
+
"A small public API for outside developers",
|
|
503
|
+
"High-volume calls between two internal services",
|
|
504
|
+
"Clients fetch large objects but only use two fields of each"
|
|
505
|
+
],
|
|
506
|
+
"correct": [
|
|
507
|
+
0,
|
|
508
|
+
1,
|
|
509
|
+
5
|
|
510
|
+
],
|
|
511
|
+
"explain": "GraphQL lets each client ask for exactly the fields it needs in one request, which fixes over-fetching and extra round trips. It has costs: one endpoint makes plain HTTP caching harder, nested fields can set off many small queries (the N+1 problem, fixed by batching), and access checks move to each field. Say \"REST by default; GraphQL when clients' needs really differ\".",
|
|
512
|
+
"source": "https://www.hellointerview.com/learn/system-design/core-concepts/api-design"
|
|
513
|
+
},
|
|
514
|
+
{
|
|
515
|
+
"id": "a-03",
|
|
516
|
+
"type": "write",
|
|
517
|
+
"bloom": "apply",
|
|
518
|
+
"prompt": "Sketch the API for a photo-sharing app: upload a photo, list one person's photos, and like a photo. For each endpoint give the method, the path and what it returns.",
|
|
519
|
+
"key_points": [
|
|
520
|
+
"Plural nouns for resources, e.g. /photos and /users/{id}/photos",
|
|
521
|
+
"The caller comes from the authentication token, never a user id in the body",
|
|
522
|
+
"Upload with POST and an idempotency key, so a retry doesn't create a second photo",
|
|
523
|
+
"The list is paged with a cursor and returns the next cursor",
|
|
524
|
+
"Liking can be repeated safely, e.g. PUT /photos/{id}/like",
|
|
525
|
+
"Status codes: 201 on create, 401 without a valid token, 404 for a missing photo"
|
|
526
|
+
],
|
|
527
|
+
"model": "POST /photos with an Idempotency-Key header returns 201 and the new photo. GET /users/{id}/photos?cursor=abc&limit=20 returns 200, a page of photos and nextCursor. PUT /photos/{id}/like returns 204; DELETE on the same path removes the like. The caller always comes from the token. 401 without a valid token, 404 if the photo is gone.",
|
|
528
|
+
"explain": "Nouns for things and HTTP methods for actions keep an API predictable. Taking the caller from the signed token stops anyone acting as someone else by editing a field in the body. Say \"the user comes from the token\" as you write each endpoint: interviewers listen for it.",
|
|
529
|
+
"source": "https://www.hellointerview.com/learn/system-design/in-a-hurry/delivery"
|
|
530
|
+
},
|
|
531
|
+
{
|
|
532
|
+
"id": "a-04",
|
|
533
|
+
"type": "multi",
|
|
534
|
+
"bloom": "understand",
|
|
535
|
+
"prompt": "Which HTTP methods are idempotent: sending the same request twice leaves the server as one request would?",
|
|
536
|
+
"options": [
|
|
537
|
+
"GET",
|
|
538
|
+
"POST",
|
|
539
|
+
"PUT",
|
|
540
|
+
"DELETE",
|
|
541
|
+
"PATCH",
|
|
542
|
+
"HEAD"
|
|
543
|
+
],
|
|
544
|
+
"correct": [
|
|
545
|
+
0,
|
|
546
|
+
2,
|
|
547
|
+
3,
|
|
548
|
+
5
|
|
549
|
+
],
|
|
550
|
+
"explain": "GET and HEAD only read. PUT sets a resource to a given state. DELETE removes it, and a second DELETE may answer 404 but changes nothing more. POST creates something new each time, and PATCH isn't guaranteed safe to repeat (think \"add 1 to the count\"). Retry the idempotent ones freely; give POST an idempotency key.",
|
|
551
|
+
"source": "https://developer.mozilla.org/en-US/docs/Glossary/Idempotent"
|
|
552
|
+
},
|
|
553
|
+
{
|
|
554
|
+
"id": "a-05",
|
|
555
|
+
"type": "duel",
|
|
556
|
+
"bloom": "evaluate",
|
|
557
|
+
"prompt": "A client sends POST /invoices. The response times out, the client retries, and the customer now has two identical invoices. What's the fix?",
|
|
558
|
+
"choices": [
|
|
559
|
+
"The client sends a unique key; the server replays the first result for repeats",
|
|
560
|
+
"Turn off retries in the client",
|
|
561
|
+
"Wait longer before retrying",
|
|
562
|
+
"Skip any new invoice that looks like a recent one"
|
|
563
|
+
],
|
|
564
|
+
"correct": 0,
|
|
565
|
+
"reasons": [
|
|
566
|
+
"A retry with the same key gets the stored first answer, so nothing is made twice",
|
|
567
|
+
"POST is idempotent already, so the key is only for logs",
|
|
568
|
+
"Keys make each request faster"
|
|
569
|
+
],
|
|
570
|
+
"reason": 0,
|
|
571
|
+
"costs": [
|
|
572
|
+
"The server stores each key and its result for a while and checks it on every create",
|
|
573
|
+
"Clients can never retry again",
|
|
574
|
+
"Every invoice needs a manual check"
|
|
575
|
+
],
|
|
576
|
+
"cost": 0,
|
|
577
|
+
"explain": "A timeout doesn't tell the client whether the invoice was made, so retrying is right, and POST isn't idempotent. A unique key per attempt (an idempotency key) lets the server spot a repeat and replay the first answer. Guessing from the contents would drop genuine look-alikes. Say \"POST with an idempotency key, kept for a day or so\".",
|
|
578
|
+
"source": "https://www.hellointerview.com/learn/system-design/core-concepts/api-design"
|
|
579
|
+
},
|
|
580
|
+
{
|
|
581
|
+
"id": "a-06",
|
|
582
|
+
"type": "duel",
|
|
583
|
+
"bloom": "evaluate",
|
|
584
|
+
"prompt": "A photo app's home timeline gets new posts every second while people scroll down it. The list endpoint takes ?offset=40&limit=20. What do you change?",
|
|
585
|
+
"choices": [
|
|
586
|
+
"Keep offset paging, because it's simpler",
|
|
587
|
+
"Switch to a cursor: an opaque token for the last item seen",
|
|
588
|
+
"Return the whole timeline in one response"
|
|
589
|
+
],
|
|
590
|
+
"correct": 1,
|
|
591
|
+
"reasons": [
|
|
592
|
+
"New posts shift every offset, so pages repeat or skip items; a cursor sticks to an item",
|
|
593
|
+
"Cursors are part of the REST standard",
|
|
594
|
+
"Offset responses can't be cached"
|
|
595
|
+
],
|
|
596
|
+
"reason": 0,
|
|
597
|
+
"costs": [
|
|
598
|
+
"You can't jump straight to page 37",
|
|
599
|
+
"Readers see more repeated posts",
|
|
600
|
+
"The client downloads every post"
|
|
601
|
+
],
|
|
602
|
+
"cost": 0,
|
|
603
|
+
"explain": "With offsets, ten new posts at the top push everything down, so the next page repeats ten the reader has already seen. A cursor means \"after this item\" and stays put. Offsets are fine for lists that rarely change, such as search results. Say \"cursor paging, because the list changes while people scroll\".",
|
|
604
|
+
"source": "https://www.hellointerview.com/learn/system-design/core-concepts/api-design"
|
|
605
|
+
},
|
|
606
|
+
{
|
|
607
|
+
"id": "a-07",
|
|
608
|
+
"type": "lightning",
|
|
609
|
+
"bloom": "remember",
|
|
610
|
+
"prompt": "Status codes. You hear what happened; give the code. 90 seconds.",
|
|
611
|
+
"seconds": 90,
|
|
612
|
+
"pairs": [
|
|
613
|
+
[
|
|
614
|
+
"A POST made a new resource",
|
|
615
|
+
"201 Created"
|
|
616
|
+
],
|
|
617
|
+
[
|
|
618
|
+
"The delete worked and there's nothing to send back",
|
|
619
|
+
"204 No Content"
|
|
620
|
+
],
|
|
621
|
+
[
|
|
622
|
+
"Your cached copy is still current",
|
|
623
|
+
"304 Not Modified"
|
|
624
|
+
],
|
|
625
|
+
[
|
|
626
|
+
"The request body is malformed",
|
|
627
|
+
"400 Bad Request"
|
|
628
|
+
],
|
|
629
|
+
[
|
|
630
|
+
"No token, or the token isn't valid",
|
|
631
|
+
"401 Unauthorized"
|
|
632
|
+
],
|
|
633
|
+
[
|
|
634
|
+
"Signed in, but not allowed to see this",
|
|
635
|
+
"403 Forbidden"
|
|
636
|
+
],
|
|
637
|
+
[
|
|
638
|
+
"There's no such resource",
|
|
639
|
+
"404 Not Found"
|
|
640
|
+
],
|
|
641
|
+
[
|
|
642
|
+
"The client is calling too often",
|
|
643
|
+
"429 Too Many Requests"
|
|
644
|
+
],
|
|
645
|
+
[
|
|
646
|
+
"An unhandled error on the server",
|
|
647
|
+
"500 Internal Server Error"
|
|
648
|
+
],
|
|
649
|
+
[
|
|
650
|
+
"The server is overloaded or down for maintenance; try later",
|
|
651
|
+
"503 Service Unavailable"
|
|
652
|
+
]
|
|
653
|
+
],
|
|
654
|
+
"explain": "4xx codes mean the caller must change something; 5xx mean the server failed and a retry may work. 401 means \"who are you?\", 403 means \"I know who you are, and no\". If one tripped you, say its pair aloud: 401 versus 403 is the usual slip.",
|
|
655
|
+
"source": "https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status"
|
|
656
|
+
},
|
|
657
|
+
{
|
|
658
|
+
"id": "a-08",
|
|
659
|
+
"type": "sticker",
|
|
660
|
+
"bloom": "apply",
|
|
661
|
+
"prompt": "A web app and a mobile app reach your services through one front door. Put each job on the box that should do it.",
|
|
662
|
+
"diagram": {
|
|
663
|
+
"width": 640,
|
|
664
|
+
"height": 300,
|
|
665
|
+
"nodes": [
|
|
666
|
+
{
|
|
667
|
+
"id": "web",
|
|
668
|
+
"label": "Web app",
|
|
669
|
+
"x": 20,
|
|
670
|
+
"y": 60
|
|
671
|
+
},
|
|
672
|
+
{
|
|
673
|
+
"id": "mob",
|
|
674
|
+
"label": "Mobile app",
|
|
675
|
+
"x": 20,
|
|
676
|
+
"y": 190
|
|
677
|
+
},
|
|
678
|
+
{
|
|
679
|
+
"id": "gw",
|
|
680
|
+
"label": "API gateway",
|
|
681
|
+
"x": 250,
|
|
682
|
+
"y": 125
|
|
683
|
+
},
|
|
684
|
+
{
|
|
685
|
+
"id": "photos",
|
|
686
|
+
"label": "Photo service",
|
|
687
|
+
"x": 490,
|
|
688
|
+
"y": 40
|
|
689
|
+
},
|
|
690
|
+
{
|
|
691
|
+
"id": "search",
|
|
692
|
+
"label": "Search service",
|
|
693
|
+
"x": 490,
|
|
694
|
+
"y": 125
|
|
695
|
+
},
|
|
696
|
+
{
|
|
697
|
+
"id": "accounts",
|
|
698
|
+
"label": "Account service",
|
|
699
|
+
"x": 490,
|
|
700
|
+
"y": 210
|
|
701
|
+
}
|
|
702
|
+
],
|
|
703
|
+
"edges": [
|
|
704
|
+
[
|
|
705
|
+
"web",
|
|
706
|
+
"gw"
|
|
707
|
+
],
|
|
708
|
+
[
|
|
709
|
+
"mob",
|
|
710
|
+
"gw"
|
|
711
|
+
],
|
|
712
|
+
[
|
|
713
|
+
"gw",
|
|
714
|
+
"photos"
|
|
715
|
+
],
|
|
716
|
+
[
|
|
717
|
+
"gw",
|
|
718
|
+
"search"
|
|
719
|
+
],
|
|
720
|
+
[
|
|
721
|
+
"gw",
|
|
722
|
+
"accounts"
|
|
723
|
+
]
|
|
724
|
+
]
|
|
725
|
+
},
|
|
726
|
+
"stickers": [
|
|
727
|
+
"Check the access token",
|
|
728
|
+
"Rate-limit each client",
|
|
729
|
+
"Route by URL path",
|
|
730
|
+
"Decrypt incoming HTTPS",
|
|
731
|
+
"Check who may see this photo",
|
|
732
|
+
"Rank search results"
|
|
733
|
+
],
|
|
734
|
+
"answers": {
|
|
735
|
+
"gw": [
|
|
736
|
+
"Check the access token",
|
|
737
|
+
"Rate-limit each client",
|
|
738
|
+
"Route by URL path",
|
|
739
|
+
"Decrypt incoming HTTPS"
|
|
740
|
+
],
|
|
741
|
+
"photos": [
|
|
742
|
+
"Check who may see this photo"
|
|
743
|
+
],
|
|
744
|
+
"search": [
|
|
745
|
+
"Rank search results"
|
|
746
|
+
]
|
|
747
|
+
},
|
|
748
|
+
"explain": "The gateway does the jobs every request needs: decrypting HTTPS (encrypted HTTP), checking who is calling, limiting how often, and routing by path. Rules about one photo, or how to rank results, need a service's own data, so they stay in that service. Say \"a gateway for routing, authentication and rate limits\" in one line, then move on.",
|
|
749
|
+
"source": "https://www.hellointerview.com/learn/system-design/deep-dives/api-gateway"
|
|
750
|
+
},
|
|
751
|
+
{
|
|
752
|
+
"id": "a-09",
|
|
753
|
+
"type": "multi",
|
|
754
|
+
"bloom": "analyse",
|
|
755
|
+
"prompt": "Which of these really need WebSockets (two-way, always open) rather than server-sent events (SSE, one-way from the server) or polling?",
|
|
756
|
+
"options": [
|
|
757
|
+
"Several people editing one document, keystrokes flowing both ways",
|
|
758
|
+
"A browser game sending each player's moves many times a second",
|
|
759
|
+
"A sales dashboard that refreshes every 5 minutes",
|
|
760
|
+
"Showing the progress of a large file export",
|
|
761
|
+
"Streaming a chatbot's answer word by word",
|
|
762
|
+
"A shared whiteboard where everyone's pen strokes appear live"
|
|
763
|
+
],
|
|
764
|
+
"correct": [
|
|
765
|
+
0,
|
|
766
|
+
1,
|
|
767
|
+
5
|
|
768
|
+
],
|
|
769
|
+
"explain": "WebSockets earn their cost when clients send often as well as receive: shared editing, games, whiteboards. Each connection is stateful, which makes load balancing and deploys harder. One-way streams suit SSE; slow refreshes suit plain polling. Ask \"does the client send often?\" If not, SSE or polling.",
|
|
770
|
+
"source": "https://www.hellointerview.com/learn/system-design/core-concepts/networking-essentials"
|
|
771
|
+
},
|
|
772
|
+
{
|
|
773
|
+
"id": "a-boss",
|
|
774
|
+
"type": "duel",
|
|
775
|
+
"bloom": "evaluate",
|
|
776
|
+
"prompt": "A build dashboard shows live progress to 20,000 viewers. Updates come every few seconds, and viewers never send anything back. A colleague proposes WebSockets. What do you choose?",
|
|
777
|
+
"choices": [
|
|
778
|
+
"WebSockets, as proposed",
|
|
779
|
+
"Server-sent events",
|
|
780
|
+
"Polling every second",
|
|
781
|
+
"Long polling"
|
|
782
|
+
],
|
|
783
|
+
"correct": 1,
|
|
784
|
+
"reasons": [
|
|
785
|
+
"Updates flow one way, and SSE does that over ordinary HTTP, reconnecting by itself",
|
|
786
|
+
"SSE lets viewers send commands back, like WebSockets",
|
|
787
|
+
"SSE needs no open connection at all",
|
|
788
|
+
"Browsers block WebSockets on dashboards"
|
|
789
|
+
],
|
|
790
|
+
"reason": 0,
|
|
791
|
+
"costs": [
|
|
792
|
+
"Each viewer keeps a connection open: 20,000 long-lived connections to run",
|
|
793
|
+
"Updates arrive later than with polling",
|
|
794
|
+
"Each viewer can only get one update",
|
|
795
|
+
"Every proxy on the path must support a new protocol"
|
|
796
|
+
],
|
|
797
|
+
"cost": 0,
|
|
798
|
+
"explain": "WebSockets bring two-way traffic you won't use, and every proxy on the path must allow the upgrade. Polling every second means 20,000 requests a second that mostly say \"nothing new\"; long polling cuts that but reconnects after every update. SSE fits a one-way stream. Say \"One-way, so SSE. I'll watch connection counts and let clients resume from the last update.\"",
|
|
799
|
+
"source": "https://www.hellointerview.com/learn/system-design/core-concepts/networking-essentials"
|
|
800
|
+
}
|
|
801
|
+
]
|
|
802
|
+
},
|
|
803
|
+
{
|
|
804
|
+
"chapter": 4,
|
|
805
|
+
"slug": "storage",
|
|
806
|
+
"title": "Storage and key technologies",
|
|
807
|
+
"emoji": "🗄️",
|
|
808
|
+
"pillar": "Key technologies",
|
|
809
|
+
"blurb": "Start with a relational database, add a store only when a requirement forces it, and say what each one costs.",
|
|
810
|
+
"boss": "stor-boss",
|
|
811
|
+
"challenges": [
|
|
812
|
+
{
|
|
813
|
+
"id": "stor-01",
|
|
814
|
+
"type": "write",
|
|
815
|
+
"bloom": "evaluate",
|
|
816
|
+
"prompt": "The interviewer asks why you chose PostgreSQL, a relational (SQL, Structured Query Language) database, over a NoSQL (non-relational) store for a photo-sharing app's users, albums and comments. Write your answer in two or three sentences.",
|
|
817
|
+
"key_points": [
|
|
818
|
+
"The data relates to itself, so joins, transactions and indexes fit it",
|
|
819
|
+
"Relational is the sound default; another store comes in when a requirement forces it",
|
|
820
|
+
"Names the requirement that would change the choice, such as a huge write volume",
|
|
821
|
+
"No sweeping claims like \"SQL doesn't scale\"",
|
|
822
|
+
"PostgreSQL goes a long way with indexes, read replicas and caching"
|
|
823
|
+
],
|
|
824
|
+
"model": "Users, albums and comments all relate to each other, so joins, transactions and indexes fit, and PostgreSQL goes a long way with indexes, read replicas and caching. I'd add another store only when a requirement forces it. If comment writes grew past what one database handles, that table is where I'd look at a wide-column store.",
|
|
825
|
+
"explain": "A clear default plus the condition that would change it shows judgement; a tour of every database shows none. Broad claims like \"NoSQL scales, SQL doesn't\" invite a hard follow-up. Say: \"Relational until a requirement forces otherwise, and here's the one that would.\"",
|
|
826
|
+
"source": "https://www.hellointerview.com/learn/system-design/core-concepts/data-modeling"
|
|
827
|
+
},
|
|
828
|
+
{
|
|
829
|
+
"id": "stor-02",
|
|
830
|
+
"type": "multi",
|
|
831
|
+
"bloom": "understand",
|
|
832
|
+
"prompt": "An invoices table is slow to answer \"show this customer's invoices\". You add an index on the customer column. Which of these are true?",
|
|
833
|
+
"options": [
|
|
834
|
+
"Lookups by customer no longer read the whole table",
|
|
835
|
+
"Every insert and update now writes to the index as well",
|
|
836
|
+
"The index takes extra disk space",
|
|
837
|
+
"Inserts get faster",
|
|
838
|
+
"Queries that filter only on the amount speed up too",
|
|
839
|
+
"It's worth indexing every column, just in case"
|
|
840
|
+
],
|
|
841
|
+
"correct": [
|
|
842
|
+
0,
|
|
843
|
+
1,
|
|
844
|
+
2
|
|
845
|
+
],
|
|
846
|
+
"explain": "An index (usually a B-tree, kept sorted) lets the database jump to matching rows instead of reading every row. The price is paid on every write, which must update each index too, plus disk space. Say: \"I'll index the columns my main queries filter on, and no more, because each index slows writes.\"",
|
|
847
|
+
"source": "https://www.hellointerview.com/learn/system-design/core-concepts/db-indexing"
|
|
848
|
+
},
|
|
849
|
+
{
|
|
850
|
+
"id": "stor-03",
|
|
851
|
+
"type": "duel",
|
|
852
|
+
"bloom": "evaluate",
|
|
853
|
+
"prompt": "Users upload photos of up to 20 megabytes (MB) each. How do the files get stored?",
|
|
854
|
+
"choices": [
|
|
855
|
+
"The app server takes the file and saves it in a database column",
|
|
856
|
+
"The phone uploads straight to object storage with a presigned URL",
|
|
857
|
+
"The app server takes the file and passes it on to object storage"
|
|
858
|
+
],
|
|
859
|
+
"correct": 1,
|
|
860
|
+
"reasons": [
|
|
861
|
+
"Big files skip your servers; the database keeps only small metadata rows",
|
|
862
|
+
"Object storage can be queried like a database",
|
|
863
|
+
"Presigned URLs never expire, so retries are simple"
|
|
864
|
+
],
|
|
865
|
+
"reason": 0,
|
|
866
|
+
"costs": [
|
|
867
|
+
"Two steps to track: the upload and its metadata row can drift apart",
|
|
868
|
+
"Every photo becomes public",
|
|
869
|
+
"None at all"
|
|
870
|
+
],
|
|
871
|
+
"cost": 0,
|
|
872
|
+
"explain": "A presigned URL is a short-lived signed web link that lets the client put one file straight into object storage, such as Amazon S3 (Simple Storage Service), with no credentials of its own. The bytes never load your servers. Keep the metadata in the database, marked pending until the upload completes. Say: \"Presigned upload to S3, metadata in Postgres.\"",
|
|
873
|
+
"source": "https://docs.aws.amazon.com/AmazonS3/latest/userguide/PresignedUrlUploadObject.html"
|
|
874
|
+
},
|
|
875
|
+
{
|
|
876
|
+
"id": "stor-04",
|
|
877
|
+
"type": "estimate",
|
|
878
|
+
"bloom": "apply",
|
|
879
|
+
"prompt": "A photo-sharing app. Uploads a day and the average photo size are below. Roughly how much object storage does a year of photos need?",
|
|
880
|
+
"generator": {
|
|
881
|
+
"kind": "storage_year",
|
|
882
|
+
"params": {
|
|
883
|
+
"items": [
|
|
884
|
+
500000,
|
|
885
|
+
20000000
|
|
886
|
+
],
|
|
887
|
+
"kb": [
|
|
888
|
+
200,
|
|
889
|
+
4000
|
|
890
|
+
]
|
|
891
|
+
}
|
|
892
|
+
},
|
|
893
|
+
"explain": "Uploads a day, times the size, times 365, then round. Two million photos at 2 MB is 4 terabytes (TB) a day, about 1.5 petabytes a year. That is object storage's job, never the database's. Say the sum out loud and round hard; the order of magnitude is what counts.",
|
|
894
|
+
"source": "https://www.hellointerview.com/learn/system-design/in-a-hurry/key-technologies"
|
|
895
|
+
},
|
|
896
|
+
{
|
|
897
|
+
"id": "stor-05",
|
|
898
|
+
"type": "duel",
|
|
899
|
+
"bloom": "apply",
|
|
900
|
+
"prompt": "Comments on photos live in DynamoDB, and each photo and comment has an ID (identifier). The main read is \"all comments on one photo, newest first\". Which primary key?",
|
|
901
|
+
"choices": [
|
|
902
|
+
"Partition key: photo ID. Sort key: time plus comment ID",
|
|
903
|
+
"Partition key: the date the comment was posted",
|
|
904
|
+
"Partition key: comment ID, then scan for the photo"
|
|
905
|
+
],
|
|
906
|
+
"correct": 0,
|
|
907
|
+
"reasons": [
|
|
908
|
+
"One query reads one partition, already in order",
|
|
909
|
+
"Dates spread writes evenly across partitions",
|
|
910
|
+
"Scans are cheap in DynamoDB"
|
|
911
|
+
],
|
|
912
|
+
"reason": 0,
|
|
913
|
+
"costs": [
|
|
914
|
+
"A viral photo's comments all land on one partition",
|
|
915
|
+
"Comments can't be sorted",
|
|
916
|
+
"Every read becomes a full-table scan"
|
|
917
|
+
],
|
|
918
|
+
"cost": 0,
|
|
919
|
+
"explain": "DynamoDB hashes the partition key to decide where an item lives; the sort key orders items inside it. Design keys around your main query, because ad-hoc questions need a planned index or a costly scan. A date key sends every new write to today's partition. Say: \"Partition by photo, sort by time, and a secondary index if we need comments by user.\"",
|
|
920
|
+
"source": "https://www.hellointerview.com/learn/system-design/deep-dives/dynamodb"
|
|
921
|
+
},
|
|
922
|
+
{
|
|
923
|
+
"id": "stor-06",
|
|
924
|
+
"type": "duel",
|
|
925
|
+
"bloom": "apply",
|
|
926
|
+
"prompt": "A photo app shows \"top 100 photos today\" by likes, updated live, plus each photo's rank. Counting with SQL on every page view is too slow. What do you reach for?",
|
|
927
|
+
"choices": [
|
|
928
|
+
"A Redis sorted set for the day: each photo's score is its like count",
|
|
929
|
+
"Cache the SQL result in app memory for an hour",
|
|
930
|
+
"Elasticsearch, sorted by likes"
|
|
931
|
+
],
|
|
932
|
+
"correct": 0,
|
|
933
|
+
"reasons": [
|
|
934
|
+
"Members stay sorted by score; updates and rank reads take log time",
|
|
935
|
+
"Redis is a durable system of record",
|
|
936
|
+
"Sorted sets take no memory"
|
|
937
|
+
],
|
|
938
|
+
"reason": 0,
|
|
939
|
+
"costs": [
|
|
940
|
+
"It lives in memory: rebuild it from the database after a failure",
|
|
941
|
+
"Ranks are only right once an hour",
|
|
942
|
+
"Each like needs a full re-sort"
|
|
943
|
+
],
|
|
944
|
+
"cost": 0,
|
|
945
|
+
"explain": "A sorted set keeps members ordered by score, so updating a score and reading the top 100 or one photo's rank are both fast. Give each day's set an expiry (TTL, time to live) so old days clean themselves up. Redis can lose recent writes in a crash, so likes still go to the database. Say: \"Sorted set for the board, database for the truth.\"",
|
|
946
|
+
"source": "https://www.hellointerview.com/learn/system-design/deep-dives/redis"
|
|
947
|
+
},
|
|
948
|
+
{
|
|
949
|
+
"id": "stor-07",
|
|
950
|
+
"type": "flaw",
|
|
951
|
+
"bloom": "analyse",
|
|
952
|
+
"prompt": "A document search product. Find the weak box, then pick the fix.",
|
|
953
|
+
"diagram": {
|
|
954
|
+
"width": 640,
|
|
955
|
+
"height": 300,
|
|
956
|
+
"nodes": [
|
|
957
|
+
{
|
|
958
|
+
"id": "client",
|
|
959
|
+
"label": "Web app",
|
|
960
|
+
"x": 20,
|
|
961
|
+
"y": 126
|
|
962
|
+
},
|
|
963
|
+
{
|
|
964
|
+
"id": "api",
|
|
965
|
+
"label": "App servers",
|
|
966
|
+
"x": 180,
|
|
967
|
+
"y": 126
|
|
968
|
+
},
|
|
969
|
+
{
|
|
970
|
+
"id": "cache",
|
|
971
|
+
"label": "Redis cache",
|
|
972
|
+
"x": 180,
|
|
973
|
+
"y": 20
|
|
974
|
+
},
|
|
975
|
+
{
|
|
976
|
+
"id": "pg",
|
|
977
|
+
"label": "PostgreSQL: users, teams",
|
|
978
|
+
"x": 380,
|
|
979
|
+
"y": 20
|
|
980
|
+
},
|
|
981
|
+
{
|
|
982
|
+
"id": "es",
|
|
983
|
+
"label": "Elasticsearch: all documents",
|
|
984
|
+
"x": 380,
|
|
985
|
+
"y": 126
|
|
986
|
+
},
|
|
987
|
+
{
|
|
988
|
+
"id": "s3",
|
|
989
|
+
"label": "Object storage: attachments",
|
|
990
|
+
"x": 380,
|
|
991
|
+
"y": 232
|
|
992
|
+
}
|
|
993
|
+
],
|
|
994
|
+
"edges": [
|
|
995
|
+
[
|
|
996
|
+
"client",
|
|
997
|
+
"api"
|
|
998
|
+
],
|
|
999
|
+
[
|
|
1000
|
+
"api",
|
|
1001
|
+
"cache"
|
|
1002
|
+
],
|
|
1003
|
+
[
|
|
1004
|
+
"api",
|
|
1005
|
+
"pg"
|
|
1006
|
+
],
|
|
1007
|
+
[
|
|
1008
|
+
"api",
|
|
1009
|
+
"es",
|
|
1010
|
+
"writes and searches"
|
|
1011
|
+
],
|
|
1012
|
+
[
|
|
1013
|
+
"api",
|
|
1014
|
+
"s3"
|
|
1015
|
+
]
|
|
1016
|
+
]
|
|
1017
|
+
},
|
|
1018
|
+
"flaw": "es",
|
|
1019
|
+
"fixes": [
|
|
1020
|
+
"Keep documents in PostgreSQL and feed Elasticsearch from it with change data capture",
|
|
1021
|
+
"Add more Elasticsearch nodes",
|
|
1022
|
+
"Put a Redis cache in front of Elasticsearch",
|
|
1023
|
+
"Drop Elasticsearch and search with SQL LIKE queries on PostgreSQL"
|
|
1024
|
+
],
|
|
1025
|
+
"fix": 0,
|
|
1026
|
+
"explain": "Elasticsearch is a search engine built on an inverted index (each word points to the documents that contain it); it shouldn't hold the only copy. Store documents in PostgreSQL and stream changes into the index with change data capture (CDC). Search then lags a little behind, so say: \"Search is a read copy, a moment behind the database.\"",
|
|
1027
|
+
"source": "https://www.hellointerview.com/learn/system-design/deep-dives/elasticsearch"
|
|
1028
|
+
},
|
|
1029
|
+
{
|
|
1030
|
+
"id": "stor-08",
|
|
1031
|
+
"type": "duel",
|
|
1032
|
+
"bloom": "evaluate",
|
|
1033
|
+
"prompt": "Every click on a product catalogue must feed three teams' jobs: search ranking, reporting and recommendations. A buggy job must be able to re-read last week's clicks. What carries the clicks?",
|
|
1034
|
+
"choices": [
|
|
1035
|
+
"A Kafka topic, with one consumer group per team",
|
|
1036
|
+
"A Redis list that each job pops clicks from",
|
|
1037
|
+
"A database table that each job polls every second"
|
|
1038
|
+
],
|
|
1039
|
+
"correct": 0,
|
|
1040
|
+
"reasons": [
|
|
1041
|
+
"Kafka keeps records for a set time; each group tracks its own offset and can rewind",
|
|
1042
|
+
"Kafka keeps one global order across the whole topic",
|
|
1043
|
+
"Kafka deletes a record once any job reads it"
|
|
1044
|
+
],
|
|
1045
|
+
"reason": 0,
|
|
1046
|
+
"costs": [
|
|
1047
|
+
"Order is kept only within a partition, so pick the key with care",
|
|
1048
|
+
"Only one team can read each click",
|
|
1049
|
+
"Clicks vanish once read"
|
|
1050
|
+
],
|
|
1051
|
+
"cost": 0,
|
|
1052
|
+
"explain": "Kafka is a durable log split into partitions. Each consumer group reads every record at its own position (offset), so teams don't slow each other and a fixed job can replay. Order is kept within a partition only: key by product if per-product order matters. Say: \"Kafka, a group per team, a week's retention.\"",
|
|
1053
|
+
"source": "https://www.hellointerview.com/learn/system-design/deep-dives/kafka"
|
|
1054
|
+
},
|
|
1055
|
+
{
|
|
1056
|
+
"id": "stor-09",
|
|
1057
|
+
"type": "lightning",
|
|
1058
|
+
"bloom": "remember",
|
|
1059
|
+
"prompt": "When you hear this, reach for what? 90 seconds.",
|
|
1060
|
+
"pairs": [
|
|
1061
|
+
[
|
|
1062
|
+
"Photos, videos and other big files",
|
|
1063
|
+
"Object storage (S3), metadata in the database"
|
|
1064
|
+
],
|
|
1065
|
+
[
|
|
1066
|
+
"Search for words inside text",
|
|
1067
|
+
"Elasticsearch (an inverted index)"
|
|
1068
|
+
],
|
|
1069
|
+
[
|
|
1070
|
+
"A live top 100 by score",
|
|
1071
|
+
"Redis sorted set"
|
|
1072
|
+
],
|
|
1073
|
+
[
|
|
1074
|
+
"Joins, transactions and a clear schema",
|
|
1075
|
+
"PostgreSQL"
|
|
1076
|
+
],
|
|
1077
|
+
[
|
|
1078
|
+
"A huge, steady, append-heavy write volume",
|
|
1079
|
+
"Cassandra (wide-column)"
|
|
1080
|
+
],
|
|
1081
|
+
[
|
|
1082
|
+
"Several teams replaying one stream of records",
|
|
1083
|
+
"Kafka"
|
|
1084
|
+
],
|
|
1085
|
+
[
|
|
1086
|
+
"Simple lookups by key at enormous scale",
|
|
1087
|
+
"DynamoDB"
|
|
1088
|
+
],
|
|
1089
|
+
[
|
|
1090
|
+
"The same data read again and again, fast",
|
|
1091
|
+
"Redis cache with a TTL"
|
|
1092
|
+
]
|
|
1093
|
+
],
|
|
1094
|
+
"explain": "Each store earns its place with one requirement. Name the requirement as you add the box, and its cost straight after: search lags, memory can be lost in a crash, scans are slow. Say: \"I'm adding X because of Y; it costs Z.\"",
|
|
1095
|
+
"source": "https://www.hellointerview.com/learn/system-design/in-a-hurry/key-technologies"
|
|
1096
|
+
},
|
|
1097
|
+
{
|
|
1098
|
+
"id": "stor-boss",
|
|
1099
|
+
"type": "fill",
|
|
1100
|
+
"bloom": "evaluate",
|
|
1101
|
+
"prompt": "A photo-sharing app. Drag the right store onto each need on the arrows: photo files, users and albums, caption search, today's top photos, view records as they happen, and a year of views written all day long.",
|
|
1102
|
+
"diagram": {
|
|
1103
|
+
"width": 640,
|
|
1104
|
+
"height": 300,
|
|
1105
|
+
"nodes": [
|
|
1106
|
+
{
|
|
1107
|
+
"id": "client",
|
|
1108
|
+
"label": "Phone app",
|
|
1109
|
+
"x": 20,
|
|
1110
|
+
"y": 126
|
|
1111
|
+
},
|
|
1112
|
+
{
|
|
1113
|
+
"id": "api",
|
|
1114
|
+
"label": "App servers",
|
|
1115
|
+
"x": 180,
|
|
1116
|
+
"y": 126
|
|
1117
|
+
},
|
|
1118
|
+
{
|
|
1119
|
+
"id": "files",
|
|
1120
|
+
"label": "?",
|
|
1121
|
+
"x": 340,
|
|
1122
|
+
"y": 20
|
|
1123
|
+
},
|
|
1124
|
+
{
|
|
1125
|
+
"id": "main",
|
|
1126
|
+
"label": "?",
|
|
1127
|
+
"x": 340,
|
|
1128
|
+
"y": 126
|
|
1129
|
+
},
|
|
1130
|
+
{
|
|
1131
|
+
"id": "stream",
|
|
1132
|
+
"label": "?",
|
|
1133
|
+
"x": 340,
|
|
1134
|
+
"y": 232
|
|
1135
|
+
},
|
|
1136
|
+
{
|
|
1137
|
+
"id": "top",
|
|
1138
|
+
"label": "?",
|
|
1139
|
+
"x": 500,
|
|
1140
|
+
"y": 20
|
|
1141
|
+
},
|
|
1142
|
+
{
|
|
1143
|
+
"id": "search",
|
|
1144
|
+
"label": "?",
|
|
1145
|
+
"x": 500,
|
|
1146
|
+
"y": 126
|
|
1147
|
+
},
|
|
1148
|
+
{
|
|
1149
|
+
"id": "wide",
|
|
1150
|
+
"label": "?",
|
|
1151
|
+
"x": 500,
|
|
1152
|
+
"y": 232
|
|
1153
|
+
}
|
|
1154
|
+
],
|
|
1155
|
+
"edges": [
|
|
1156
|
+
[
|
|
1157
|
+
"client",
|
|
1158
|
+
"api"
|
|
1159
|
+
],
|
|
1160
|
+
[
|
|
1161
|
+
"client",
|
|
1162
|
+
"files",
|
|
1163
|
+
"photo files"
|
|
1164
|
+
],
|
|
1165
|
+
[
|
|
1166
|
+
"api",
|
|
1167
|
+
"main",
|
|
1168
|
+
"users, albums, likes"
|
|
1169
|
+
],
|
|
1170
|
+
[
|
|
1171
|
+
"api",
|
|
1172
|
+
"top",
|
|
1173
|
+
"today's top photos"
|
|
1174
|
+
],
|
|
1175
|
+
[
|
|
1176
|
+
"main",
|
|
1177
|
+
"search",
|
|
1178
|
+
"caption search copy"
|
|
1179
|
+
],
|
|
1180
|
+
[
|
|
1181
|
+
"api",
|
|
1182
|
+
"stream",
|
|
1183
|
+
"view records"
|
|
1184
|
+
],
|
|
1185
|
+
[
|
|
1186
|
+
"stream",
|
|
1187
|
+
"wide",
|
|
1188
|
+
"a year of views, write-heavy"
|
|
1189
|
+
]
|
|
1190
|
+
]
|
|
1191
|
+
},
|
|
1192
|
+
"slots": {
|
|
1193
|
+
"files": "Object storage (S3)",
|
|
1194
|
+
"main": "PostgreSQL",
|
|
1195
|
+
"top": "Redis sorted set",
|
|
1196
|
+
"search": "Elasticsearch",
|
|
1197
|
+
"stream": "Kafka",
|
|
1198
|
+
"wide": "Cassandra"
|
|
1199
|
+
},
|
|
1200
|
+
"palette": [
|
|
1201
|
+
"Object storage (S3)",
|
|
1202
|
+
"PostgreSQL",
|
|
1203
|
+
"Redis sorted set",
|
|
1204
|
+
"Elasticsearch",
|
|
1205
|
+
"Kafka",
|
|
1206
|
+
"Cassandra",
|
|
1207
|
+
"Photos in a PostgreSQL column",
|
|
1208
|
+
"Redis as the main database",
|
|
1209
|
+
"Graph database"
|
|
1210
|
+
],
|
|
1211
|
+
"explain": "Files go to object storage; related rows to PostgreSQL; word search to Elasticsearch fed from Postgres; a live top list to a Redis sorted set; a replayable stream to Kafka; heavy append-only writes to Cassandra. Photos in a database column bloat it; Redis as the truth can lose data. Say each box's reason as you place it.",
|
|
1212
|
+
"source": "https://www.hellointerview.com/learn/system-design/in-a-hurry/key-technologies"
|
|
1213
|
+
}
|
|
1214
|
+
]
|
|
1215
|
+
},
|
|
1216
|
+
{
|
|
1217
|
+
"chapter": 5,
|
|
1218
|
+
"slug": "caching-scaling",
|
|
1219
|
+
"title": "Caching and scaling",
|
|
1220
|
+
"emoji": "⚡",
|
|
1221
|
+
"pillar": "Core concepts",
|
|
1222
|
+
"blurb": "Pick a caching pattern, keep it fresh, survive hot keys and stampedes, then scale out with replicas, shards and consistent hashing.",
|
|
1223
|
+
"boss": "scal-boss",
|
|
1224
|
+
"challenges": [
|
|
1225
|
+
{
|
|
1226
|
+
"id": "scal-01",
|
|
1227
|
+
"type": "sticker",
|
|
1228
|
+
"bloom": "understand",
|
|
1229
|
+
"prompt": "Three ways to put a cache in front of a database. Stick each trait on the pattern it belongs to.",
|
|
1230
|
+
"diagram": {
|
|
1231
|
+
"width": 640,
|
|
1232
|
+
"height": 200,
|
|
1233
|
+
"nodes": [
|
|
1234
|
+
{
|
|
1235
|
+
"id": "aside",
|
|
1236
|
+
"label": "Cache-aside",
|
|
1237
|
+
"x": 40,
|
|
1238
|
+
"y": 76
|
|
1239
|
+
},
|
|
1240
|
+
{
|
|
1241
|
+
"id": "through",
|
|
1242
|
+
"label": "Write-through",
|
|
1243
|
+
"x": 260,
|
|
1244
|
+
"y": 76
|
|
1245
|
+
},
|
|
1246
|
+
{
|
|
1247
|
+
"id": "back",
|
|
1248
|
+
"label": "Write-back",
|
|
1249
|
+
"x": 480,
|
|
1250
|
+
"y": 76
|
|
1251
|
+
}
|
|
1252
|
+
]
|
|
1253
|
+
},
|
|
1254
|
+
"stickers": [
|
|
1255
|
+
"App fills the cache on a miss",
|
|
1256
|
+
"Only data that's read gets cached",
|
|
1257
|
+
"Each write waits for cache and database",
|
|
1258
|
+
"Fastest writes",
|
|
1259
|
+
"A cache crash can lose writes"
|
|
1260
|
+
],
|
|
1261
|
+
"answers": {
|
|
1262
|
+
"aside": [
|
|
1263
|
+
"App fills the cache on a miss",
|
|
1264
|
+
"Only data that's read gets cached"
|
|
1265
|
+
],
|
|
1266
|
+
"through": [
|
|
1267
|
+
"Each write waits for cache and database"
|
|
1268
|
+
],
|
|
1269
|
+
"back": [
|
|
1270
|
+
"Fastest writes",
|
|
1271
|
+
"A cache crash can lose writes"
|
|
1272
|
+
]
|
|
1273
|
+
},
|
|
1274
|
+
"explain": "Cache-aside is the usual default: the app reads the cache, and on a miss loads from the database and fills it. Write-through keeps the cache current but makes each write wait for both. Write-back (also called write-behind) answers from the cache and flushes later: fast, but a crash loses whatever hadn't flushed. Name the pattern and why in one sentence.",
|
|
1275
|
+
"source": "https://github.com/donnemartin/system-design-primer"
|
|
1276
|
+
},
|
|
1277
|
+
{
|
|
1278
|
+
"id": "scal-02",
|
|
1279
|
+
"type": "duel",
|
|
1280
|
+
"bloom": "evaluate",
|
|
1281
|
+
"prompt": "Product pages are cached with cache-aside and read thousands of times a minute; a page changes a few times a day. When someone edits a page, what does the write path do?",
|
|
1282
|
+
"choices": [
|
|
1283
|
+
"Update the database, then delete the cached copy",
|
|
1284
|
+
"Update the database and wait for the cached copy to expire",
|
|
1285
|
+
"Update the cache now and the database later"
|
|
1286
|
+
],
|
|
1287
|
+
"correct": 0,
|
|
1288
|
+
"reasons": [
|
|
1289
|
+
"The next read reloads from the database, so no second copy needs updating",
|
|
1290
|
+
"A cache can never serve stale data",
|
|
1291
|
+
"Writing the cache first makes the database optional"
|
|
1292
|
+
],
|
|
1293
|
+
"reason": 0,
|
|
1294
|
+
"costs": [
|
|
1295
|
+
"One extra miss per edit, and a brief window where a reader may see the old page",
|
|
1296
|
+
"Readers see the old page for up to an hour",
|
|
1297
|
+
"Edits can be lost if the cache crashes"
|
|
1298
|
+
],
|
|
1299
|
+
"cost": 0,
|
|
1300
|
+
"explain": "Deleting on write keeps one source of truth, and the next read refills the cache. Keep an expiry (TTL, time to live) on every key anyway, so a missed delete corrects itself. Waiting for expiry shows stale pages for the whole TTL; writing the cache first risks lost edits. Say: \"Write the database, delete the key, TTL as a safety net.\"",
|
|
1301
|
+
"source": "https://aws.amazon.com/caching/best-practices/"
|
|
1302
|
+
},
|
|
1303
|
+
{
|
|
1304
|
+
"id": "scal-03",
|
|
1305
|
+
"type": "multi",
|
|
1306
|
+
"bloom": "analyse",
|
|
1307
|
+
"prompt": "The home page's cached data is read 50,000 times a second. Each time its key expires, thousands of requests miss at once and the database falls over. Which fixes help?",
|
|
1308
|
+
"options": [
|
|
1309
|
+
"Let one request rebuild the value while the rest wait for its result",
|
|
1310
|
+
"Refresh the value in the background just before it expires",
|
|
1311
|
+
"Shorten the expiry so the value is always fresh",
|
|
1312
|
+
"Add app servers to absorb the extra misses",
|
|
1313
|
+
"Turn the cache off while the value rebuilds"
|
|
1314
|
+
],
|
|
1315
|
+
"correct": [
|
|
1316
|
+
0,
|
|
1317
|
+
1
|
|
1318
|
+
],
|
|
1319
|
+
"explain": "This is a cache stampede: one popular key expires and every request rebuilds it at once. Coalescing lets one request rebuild while the rest share its answer; refreshing early means the key never expires under load. A shorter expiry brings stampedes more often, and more app servers don't shield the database. Say: \"Coalesce the rebuild, and refresh the hottest keys early.\"",
|
|
1320
|
+
"source": "https://www.hellointerview.com/learn/system-design/core-concepts/caching"
|
|
1321
|
+
},
|
|
1322
|
+
{
|
|
1323
|
+
"id": "scal-04",
|
|
1324
|
+
"type": "recall",
|
|
1325
|
+
"bloom": "analyse",
|
|
1326
|
+
"prompt": "A deploy cached 50,000 product pages in the same minute, all with a one-hour TTL. Now database load spikes at that same minute every hour. Why, and what's the fix?",
|
|
1327
|
+
"answer": "They all expire together, so the misses arrive in one burst. Add a random offset to each TTL (an hour plus up to a couple of minutes) so expiries spread out.",
|
|
1328
|
+
"explain": "Identical expiry times line the misses up into one spike. Random jitter on each TTL spreads them into a trickle, at almost no cost. Say: \"TTL plus a little jitter, so popular keys don't all expire at once.\"",
|
|
1329
|
+
"source": "https://aws.amazon.com/caching/best-practices/"
|
|
1330
|
+
},
|
|
1331
|
+
{
|
|
1332
|
+
"id": "scal-05",
|
|
1333
|
+
"type": "duel",
|
|
1334
|
+
"bloom": "evaluate",
|
|
1335
|
+
"prompt": "One viral video's details are read 300,000 times a second. The Redis node that owns that key is maxed out while the other nodes sit idle. Best move?",
|
|
1336
|
+
"choices": [
|
|
1337
|
+
"Keep several copies under suffixed keys and read a random one",
|
|
1338
|
+
"Add more nodes to the Redis cluster",
|
|
1339
|
+
"Give the key a shorter expiry"
|
|
1340
|
+
],
|
|
1341
|
+
"correct": 0,
|
|
1342
|
+
"reasons": [
|
|
1343
|
+
"One key always lands on one node, so new nodes never share its load",
|
|
1344
|
+
"Short expiries spread a key across nodes",
|
|
1345
|
+
"Redis gets faster as keys get shorter"
|
|
1346
|
+
],
|
|
1347
|
+
"reason": 0,
|
|
1348
|
+
"costs": [
|
|
1349
|
+
"An update must reach every copy, or readers briefly disagree",
|
|
1350
|
+
"Reads get slower for every other key",
|
|
1351
|
+
"None"
|
|
1352
|
+
],
|
|
1353
|
+
"cost": 0,
|
|
1354
|
+
"explain": "A hot key overloads the one node that owns it, however big the cluster. Copies under keys like video42-1 to video42-8 spread reads over several nodes, and a small in-process cache on each app server helps too. The cost is updating every copy. Say: \"Hot key, so I'll copy it and spread the reads.\"",
|
|
1355
|
+
"source": "https://www.hellointerview.com/learn/system-design/deep-dives/redis"
|
|
1356
|
+
},
|
|
1357
|
+
{
|
|
1358
|
+
"id": "scal-06",
|
|
1359
|
+
"type": "lightning",
|
|
1360
|
+
"bloom": "remember",
|
|
1361
|
+
"prompt": "When you hear this, reach for what? 90 seconds.",
|
|
1362
|
+
"pairs": [
|
|
1363
|
+
[
|
|
1364
|
+
"Images and scripts for users all over the world",
|
|
1365
|
+
"A CDN (content delivery network)"
|
|
1366
|
+
],
|
|
1367
|
+
[
|
|
1368
|
+
"Users are logged out when one server restarts",
|
|
1369
|
+
"Stateless servers, sessions in a shared store"
|
|
1370
|
+
],
|
|
1371
|
+
[
|
|
1372
|
+
"The biggest machine size is already bought",
|
|
1373
|
+
"Scale out: add more machines"
|
|
1374
|
+
],
|
|
1375
|
+
[
|
|
1376
|
+
"Reads outgrow one database; writes are fine",
|
|
1377
|
+
"Read replicas"
|
|
1378
|
+
],
|
|
1379
|
+
[
|
|
1380
|
+
"Writes or data outgrow one database",
|
|
1381
|
+
"Shard the data"
|
|
1382
|
+
],
|
|
1383
|
+
[
|
|
1384
|
+
"Adding a cache node reshuffles almost every key",
|
|
1385
|
+
"Consistent hashing"
|
|
1386
|
+
],
|
|
1387
|
+
[
|
|
1388
|
+
"Writes must be very fast; losing a few is acceptable",
|
|
1389
|
+
"Write-back (write-behind) cache"
|
|
1390
|
+
],
|
|
1391
|
+
[
|
|
1392
|
+
"The same data is read again and again",
|
|
1393
|
+
"Cache-aside in front of the database"
|
|
1394
|
+
]
|
|
1395
|
+
],
|
|
1396
|
+
"explain": "Each symptom has a standard first move. Scaling up (a bigger machine) is simplest but hits a ceiling and stays one point of failure; scaling out needs stateless servers so any of them can take any request. Say the symptom, then the move: \"Reads are the bottleneck, so replicas.\"",
|
|
1397
|
+
"source": "https://github.com/donnemartin/system-design-primer"
|
|
1398
|
+
},
|
|
1399
|
+
{
|
|
1400
|
+
"id": "scal-07",
|
|
1401
|
+
"type": "write",
|
|
1402
|
+
"bloom": "analyse",
|
|
1403
|
+
"prompt": "You added read replicas. Now a user edits their profile, refreshes, and sees the old version for a second or two. Explain what's going on and how you'd fix it.",
|
|
1404
|
+
"key_points": [
|
|
1405
|
+
"Replicas copy from the primary asynchronously, so they run a little behind",
|
|
1406
|
+
"The refresh read hit a replica that hadn't caught up yet",
|
|
1407
|
+
"Read-your-own-writes: send that user's reads to the primary for a short while after a write",
|
|
1408
|
+
"Other users keep reading replicas; slight staleness is fine for them",
|
|
1409
|
+
"The cost: some read load returns to the primary"
|
|
1410
|
+
],
|
|
1411
|
+
"model": "Replication is asynchronous, so a replica can be a second behind, and the refresh landed on one that hadn't caught up. I'd give the author read-your-own-writes: for a few seconds after an edit, their own reads go to the primary. Everyone else keeps reading replicas, where a second of staleness is fine. The cost is a little extra load on the primary.",
|
|
1412
|
+
"explain": "Replication lag is normal; the person who just wrote is the one who notices it. Sending only their reads to the primary fixes what they see without giving up the replicas, and adding replicas doesn't remove the lag. Say: \"Read-your-own-writes for the author, replicas for everyone else.\"",
|
|
1413
|
+
"source": "https://www.hellointerview.com/learn/system-design/core-concepts/cap-theorem"
|
|
1414
|
+
},
|
|
1415
|
+
{
|
|
1416
|
+
"id": "scal-08",
|
|
1417
|
+
"type": "duel",
|
|
1418
|
+
"bloom": "apply",
|
|
1419
|
+
"prompt": "An invoicing product's invoices table has outgrown one database. Most queries ask for \"this customer's invoices\". What's the shard key?",
|
|
1420
|
+
"choices": [
|
|
1421
|
+
"The customer's identifier",
|
|
1422
|
+
"The date the invoice was created",
|
|
1423
|
+
"Paid or unpaid"
|
|
1424
|
+
],
|
|
1425
|
+
"correct": 0,
|
|
1426
|
+
"reasons": [
|
|
1427
|
+
"Many distinct values spread load evenly, and the main query hits one shard",
|
|
1428
|
+
"Recent invoices sit together, which speeds up every write",
|
|
1429
|
+
"Few distinct values keep routing simple"
|
|
1430
|
+
],
|
|
1431
|
+
"reason": 0,
|
|
1432
|
+
"costs": [
|
|
1433
|
+
"One huge customer can make its shard hot",
|
|
1434
|
+
"All new writes land on the newest shard",
|
|
1435
|
+
"Every query must visit every shard"
|
|
1436
|
+
],
|
|
1437
|
+
"cost": 0,
|
|
1438
|
+
"explain": "A good shard key has many values, spreads load evenly and fits the main query. A date sends every new write to one shard; a yes-or-no field gives two shards at most. The customer key's risk is a hot partition from one giant customer: give it its own shard. Reports across all customers now hit every shard, so precompute them. Say the key and its risk together.",
|
|
1439
|
+
"source": "https://www.hellointerview.com/learn/system-design/core-concepts/sharding"
|
|
1440
|
+
},
|
|
1441
|
+
{
|
|
1442
|
+
"id": "scal-09",
|
|
1443
|
+
"type": "multi",
|
|
1444
|
+
"bloom": "analyse",
|
|
1445
|
+
"prompt": "Ten cache servers place keys with hash(key) mod 10. You add an eleventh and switch to mod 11. Which of these are true?",
|
|
1446
|
+
"options": [
|
|
1447
|
+
"About 90% of keys now belong to a different server",
|
|
1448
|
+
"The cache hit rate drops sharply until it refills",
|
|
1449
|
+
"With consistent hashing, only about 1 key in 11 would move",
|
|
1450
|
+
"With mod N, only the keys bound for the new server move",
|
|
1451
|
+
"Consistent hashing moves no keys at all"
|
|
1452
|
+
],
|
|
1453
|
+
"correct": [
|
|
1454
|
+
0,
|
|
1455
|
+
1,
|
|
1456
|
+
2
|
|
1457
|
+
],
|
|
1458
|
+
"explain": "With mod N, changing N changes almost every key's remainder: about 10 keys in 11 move, and the cache goes nearly cold. On a consistent-hashing ring, a new server takes only the keys between it and its neighbour, about 1 in 11. Say: \"Consistent hashing, so adding a node moves about a tenth of the keys, not nine tenths.\"",
|
|
1459
|
+
"source": "https://en.wikipedia.org/wiki/Consistent_hashing"
|
|
1460
|
+
},
|
|
1461
|
+
{
|
|
1462
|
+
"id": "scal-boss",
|
|
1463
|
+
"type": "flaw",
|
|
1464
|
+
"bloom": "evaluate",
|
|
1465
|
+
"prompt": "Fix this at ten times the load. A link shortener: reads come from the cache and replicas, and the primary database already runs at 70% busy on today's writes alone. Find the box that breaks first, then pick the fix.",
|
|
1466
|
+
"diagram": {
|
|
1467
|
+
"width": 640,
|
|
1468
|
+
"height": 300,
|
|
1469
|
+
"nodes": [
|
|
1470
|
+
{
|
|
1471
|
+
"id": "users",
|
|
1472
|
+
"label": "Users",
|
|
1473
|
+
"x": 20,
|
|
1474
|
+
"y": 126
|
|
1475
|
+
},
|
|
1476
|
+
{
|
|
1477
|
+
"id": "cdn",
|
|
1478
|
+
"label": "CDN: site assets",
|
|
1479
|
+
"x": 170,
|
|
1480
|
+
"y": 20
|
|
1481
|
+
},
|
|
1482
|
+
{
|
|
1483
|
+
"id": "lb",
|
|
1484
|
+
"label": "Load balancer",
|
|
1485
|
+
"x": 170,
|
|
1486
|
+
"y": 126
|
|
1487
|
+
},
|
|
1488
|
+
{
|
|
1489
|
+
"id": "cache",
|
|
1490
|
+
"label": "Redis cluster",
|
|
1491
|
+
"x": 320,
|
|
1492
|
+
"y": 20
|
|
1493
|
+
},
|
|
1494
|
+
{
|
|
1495
|
+
"id": "app",
|
|
1496
|
+
"label": "Stateless app servers",
|
|
1497
|
+
"x": 320,
|
|
1498
|
+
"y": 126
|
|
1499
|
+
},
|
|
1500
|
+
{
|
|
1501
|
+
"id": "primary",
|
|
1502
|
+
"label": "Primary: all writes",
|
|
1503
|
+
"x": 480,
|
|
1504
|
+
"y": 126
|
|
1505
|
+
},
|
|
1506
|
+
{
|
|
1507
|
+
"id": "replicas",
|
|
1508
|
+
"label": "Read replicas x3",
|
|
1509
|
+
"x": 480,
|
|
1510
|
+
"y": 232
|
|
1511
|
+
}
|
|
1512
|
+
],
|
|
1513
|
+
"edges": [
|
|
1514
|
+
[
|
|
1515
|
+
"users",
|
|
1516
|
+
"cdn",
|
|
1517
|
+
"assets"
|
|
1518
|
+
],
|
|
1519
|
+
[
|
|
1520
|
+
"users",
|
|
1521
|
+
"lb"
|
|
1522
|
+
],
|
|
1523
|
+
[
|
|
1524
|
+
"lb",
|
|
1525
|
+
"app"
|
|
1526
|
+
],
|
|
1527
|
+
[
|
|
1528
|
+
"app",
|
|
1529
|
+
"cache",
|
|
1530
|
+
"link lookups"
|
|
1531
|
+
],
|
|
1532
|
+
[
|
|
1533
|
+
"app",
|
|
1534
|
+
"primary",
|
|
1535
|
+
"new links"
|
|
1536
|
+
],
|
|
1537
|
+
[
|
|
1538
|
+
"primary",
|
|
1539
|
+
"replicas",
|
|
1540
|
+
"copies"
|
|
1541
|
+
],
|
|
1542
|
+
[
|
|
1543
|
+
"app",
|
|
1544
|
+
"replicas",
|
|
1545
|
+
"cache misses"
|
|
1546
|
+
]
|
|
1547
|
+
]
|
|
1548
|
+
},
|
|
1549
|
+
"flaw": "primary",
|
|
1550
|
+
"fixes": [
|
|
1551
|
+
"Shard by short code with consistent hashing; each shard keeps its own replicas",
|
|
1552
|
+
"Add more read replicas",
|
|
1553
|
+
"Add more stateless app servers",
|
|
1554
|
+
"Grow the Redis cluster",
|
|
1555
|
+
"Move the primary to a machine twice the size"
|
|
1556
|
+
],
|
|
1557
|
+
"fix": 0,
|
|
1558
|
+
"explain": "Reads already scale out through the CDN, cache and replicas, but every write still goes through one primary, and ten times 70% is seven machines' worth. Replicas add read capacity, not write capacity; a machine twice the size covers two. Shard by short code, which every lookup carries, with consistent hashing so later shards move few keys. Say: \"Writes are the wall, so shard.\"",
|
|
1559
|
+
"source": "https://www.hellointerview.com/learn/system-design/core-concepts/sharding"
|
|
1560
|
+
}
|
|
1561
|
+
]
|
|
1562
|
+
},
|
|
1563
|
+
{
|
|
1564
|
+
"chapter": 6,
|
|
1565
|
+
"slug": "consistency",
|
|
1566
|
+
"title": "Consistency and CAP",
|
|
1567
|
+
"emoji": "⚖️",
|
|
1568
|
+
"pillar": "Core concepts",
|
|
1569
|
+
"blurb": "Know what CAP really forces, then choose strong or eventual consistency feature by feature, never for the whole system.",
|
|
1570
|
+
"boss": "cons-boss",
|
|
1571
|
+
"challenges": [
|
|
1572
|
+
{
|
|
1573
|
+
"id": "cons-01",
|
|
1574
|
+
"type": "recall",
|
|
1575
|
+
"bloom": "remember",
|
|
1576
|
+
"prompt": "In one breath: what does the CAP theorem (consistency, availability, partition tolerance) actually force you to choose?",
|
|
1577
|
+
"answer": "When the network splits between nodes, a distributed system must either refuse some requests to stay consistent or keep answering and risk stale data. Splits will happen, so partition tolerance isn't optional.",
|
|
1578
|
+
"explain": "\"Pick two of three\" misleads, because no network is immune to splits. The real choice bites only during a split: stay correct or stay up. Say: \"Partitions happen, so for this feature I'd favour consistency (or availability), because…\"",
|
|
1579
|
+
"source": "https://en.wikipedia.org/wiki/CAP_theorem"
|
|
1580
|
+
},
|
|
1581
|
+
{
|
|
1582
|
+
"id": "cons-02",
|
|
1583
|
+
"type": "multi",
|
|
1584
|
+
"bloom": "understand",
|
|
1585
|
+
"prompt": "Which of these are true? (ACID is the atomicity, consistency, isolation and durability that database transactions promise.)",
|
|
1586
|
+
"options": [
|
|
1587
|
+
"Network splits will happen, so the real choice is consistency or availability during one",
|
|
1588
|
+
"A system can pick consistency and availability and skip partition tolerance",
|
|
1589
|
+
"CAP's C means every read sees the latest write, or gets an error",
|
|
1590
|
+
"CAP's C is the same promise as the C in ACID",
|
|
1591
|
+
"ACID's C means a transaction leaves the data obeying its rules, such as constraints",
|
|
1592
|
+
"Choosing availability means reads are never stale"
|
|
1593
|
+
],
|
|
1594
|
+
"correct": [
|
|
1595
|
+
0,
|
|
1596
|
+
2,
|
|
1597
|
+
4
|
|
1598
|
+
],
|
|
1599
|
+
"explain": "Same letter, different promise. CAP's C is about copies agreeing, so a read reflects the latest write. ACID's C is about one database's rules still holding after a transaction. Choosing availability means accepting stale reads during a split. Say which C you mean when you use the word.",
|
|
1600
|
+
"source": "https://en.wikipedia.org/wiki/CAP_theorem"
|
|
1601
|
+
},
|
|
1602
|
+
{
|
|
1603
|
+
"id": "cons-03",
|
|
1604
|
+
"type": "recall",
|
|
1605
|
+
"bloom": "understand",
|
|
1606
|
+
"prompt": "CAP only talks about network splits. What does PACELC (if Partition, Availability or Consistency; Else, Latency or Consistency) add, and why does it matter on a normal day?",
|
|
1607
|
+
"answer": "The \"else\": with no split at all, you still trade latency against consistency. Waiting for replicas to confirm a write, or to agree on a read, makes every request slower.",
|
|
1608
|
+
"explain": "Splits are rare, but the latency cost of strong consistency is paid on every request. That's why many databases default to eventually consistent reads. Say: \"Even without a partition, strong reads cost latency, so I'd only pay that where a stale read hurts.\"",
|
|
1609
|
+
"source": "https://en.wikipedia.org/wiki/PACELC_design_principle"
|
|
1610
|
+
},
|
|
1611
|
+
{
|
|
1612
|
+
"id": "cons-04",
|
|
1613
|
+
"type": "duel",
|
|
1614
|
+
"bloom": "evaluate",
|
|
1615
|
+
"prompt": "A team workspace product has prepaid credit balances, invoices, an activity feed and like counts. The interviewer asks: \"Is your system CP or AP?\" (consistent or available when the network splits). Best answer?",
|
|
1616
|
+
"choices": [
|
|
1617
|
+
"\"CP: correctness matters most.\"",
|
|
1618
|
+
"\"AP: uptime matters most.\"",
|
|
1619
|
+
"\"Neither as a whole: strong for balances and invoices, eventual for feed and likes.\""
|
|
1620
|
+
],
|
|
1621
|
+
"correct": 2,
|
|
1622
|
+
"reasons": [
|
|
1623
|
+
"A stale read costs a different amount in each feature, so each gets its own choice",
|
|
1624
|
+
"Mixed models are always cheaper to run",
|
|
1625
|
+
"Interviewers mark down any system that is fully CP"
|
|
1626
|
+
],
|
|
1627
|
+
"reason": 0,
|
|
1628
|
+
"costs": [
|
|
1629
|
+
"Two consistency models to explain, test and operate",
|
|
1630
|
+
"Every read in the system gets slower",
|
|
1631
|
+
"The feed can no longer be cached"
|
|
1632
|
+
],
|
|
1633
|
+
"cost": 0,
|
|
1634
|
+
"explain": "One label for the whole system forces the wrong trade on half the features: a stale balance costs money, a stale like count costs nothing. Ask per feature \"what breaks if this read is a few seconds old?\" Say: \"Strong where money or totals live, eventual everywhere else.\"",
|
|
1635
|
+
"source": "https://www.hellointerview.com/learn/system-design/in-a-hurry/core-concepts"
|
|
1636
|
+
},
|
|
1637
|
+
{
|
|
1638
|
+
"id": "cons-05",
|
|
1639
|
+
"type": "fill",
|
|
1640
|
+
"bloom": "apply",
|
|
1641
|
+
"prompt": "Fill the boxes. A confirmed write must survive losing the leader, and balance reads must see the latest write. Feed reads can lag a few seconds.",
|
|
1642
|
+
"diagram": {
|
|
1643
|
+
"width": 640,
|
|
1644
|
+
"height": 300,
|
|
1645
|
+
"nodes": [
|
|
1646
|
+
{
|
|
1647
|
+
"id": "app",
|
|
1648
|
+
"label": "App server",
|
|
1649
|
+
"x": 20,
|
|
1650
|
+
"y": 126
|
|
1651
|
+
},
|
|
1652
|
+
{
|
|
1653
|
+
"id": "s-leader",
|
|
1654
|
+
"label": "?",
|
|
1655
|
+
"x": 250,
|
|
1656
|
+
"y": 40
|
|
1657
|
+
},
|
|
1658
|
+
{
|
|
1659
|
+
"id": "s-sync",
|
|
1660
|
+
"label": "?",
|
|
1661
|
+
"x": 490,
|
|
1662
|
+
"y": 40
|
|
1663
|
+
},
|
|
1664
|
+
{
|
|
1665
|
+
"id": "s-async",
|
|
1666
|
+
"label": "?",
|
|
1667
|
+
"x": 250,
|
|
1668
|
+
"y": 222
|
|
1669
|
+
}
|
|
1670
|
+
],
|
|
1671
|
+
"edges": [
|
|
1672
|
+
[
|
|
1673
|
+
"app",
|
|
1674
|
+
"s-leader",
|
|
1675
|
+
"writes, balance reads"
|
|
1676
|
+
],
|
|
1677
|
+
[
|
|
1678
|
+
"s-leader",
|
|
1679
|
+
"s-sync",
|
|
1680
|
+
"waits for its confirmation"
|
|
1681
|
+
],
|
|
1682
|
+
[
|
|
1683
|
+
"s-leader",
|
|
1684
|
+
"s-async",
|
|
1685
|
+
"copies later"
|
|
1686
|
+
],
|
|
1687
|
+
[
|
|
1688
|
+
"app",
|
|
1689
|
+
"s-async",
|
|
1690
|
+
"feed reads"
|
|
1691
|
+
]
|
|
1692
|
+
]
|
|
1693
|
+
},
|
|
1694
|
+
"slots": {
|
|
1695
|
+
"s-leader": "Leader database",
|
|
1696
|
+
"s-sync": "Synchronous replica",
|
|
1697
|
+
"s-async": "Asynchronous replica"
|
|
1698
|
+
},
|
|
1699
|
+
"palette": [
|
|
1700
|
+
"Leader database",
|
|
1701
|
+
"Synchronous replica",
|
|
1702
|
+
"Asynchronous replica",
|
|
1703
|
+
"Cache",
|
|
1704
|
+
"Message queue",
|
|
1705
|
+
"Load balancer"
|
|
1706
|
+
],
|
|
1707
|
+
"explain": "A synchronous replica holds every confirmed write, so promoting it loses nothing, but each write waits for it. An asynchronous one keeps writes fast and serves reads that may lag, which suits the feed. Say: \"One synchronous replica for safety, asynchronous ones for read scale.\"",
|
|
1708
|
+
"source": "https://github.com/donnemartin/system-design-primer"
|
|
1709
|
+
},
|
|
1710
|
+
{
|
|
1711
|
+
"id": "cons-06",
|
|
1712
|
+
"type": "multi",
|
|
1713
|
+
"bloom": "analyse",
|
|
1714
|
+
"prompt": "Five replicas (N = 5). A write counts once W replicas confirm it; a read asks R replicas. Which settings guarantee every read overlaps the latest confirmed write?",
|
|
1715
|
+
"options": [
|
|
1716
|
+
"W = 3, R = 3",
|
|
1717
|
+
"W = 2, R = 2",
|
|
1718
|
+
"W = 5, R = 1",
|
|
1719
|
+
"W = 1, R = 1",
|
|
1720
|
+
"W = 4, R = 2",
|
|
1721
|
+
"W = 2, R = 3"
|
|
1722
|
+
],
|
|
1723
|
+
"correct": [
|
|
1724
|
+
0,
|
|
1725
|
+
2,
|
|
1726
|
+
4
|
|
1727
|
+
],
|
|
1728
|
+
"explain": "Reads and writes overlap when R + W > N: at least one replica in every read saw the latest write. 2 + 3 = 5 is not enough. W = 5 makes reads cheap, but a write fails if any replica is down. Say: \"A quorum: R plus W greater than N, so every read overlaps the last write.\"",
|
|
1729
|
+
"source": "https://en.wikipedia.org/wiki/Quorum_(distributed_computing)"
|
|
1730
|
+
},
|
|
1731
|
+
{
|
|
1732
|
+
"id": "cons-07",
|
|
1733
|
+
"type": "multi",
|
|
1734
|
+
"bloom": "analyse",
|
|
1735
|
+
"prompt": "A user renames their workspace, reloads, and sees the old name. Reads go to replicas that are updated asynchronously. Which two fixes give that user read-your-writes without making every read strong?",
|
|
1736
|
+
"options": [
|
|
1737
|
+
"For a short time after a user writes, send that user's reads to the leader",
|
|
1738
|
+
"Ask for a strongly consistent read only on the page the user just edited",
|
|
1739
|
+
"Send every read in the system to the leader",
|
|
1740
|
+
"Put a cache in front of the replicas",
|
|
1741
|
+
"Add more replicas",
|
|
1742
|
+
"Make the rename asynchronous too"
|
|
1743
|
+
],
|
|
1744
|
+
"correct": [
|
|
1745
|
+
0,
|
|
1746
|
+
1
|
|
1747
|
+
],
|
|
1748
|
+
"explain": "Read-your-writes only promises that writers see their own change; everyone else can lag. So aim the fix at that user and that page. Some databases let you choose per read: DynamoDB reads are eventually consistent by default, strongly consistent on request at twice the cost. Say: \"Read-your-writes for the editor, eventual for everyone else.\"",
|
|
1749
|
+
"source": "https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.ReadConsistency.html"
|
|
1750
|
+
},
|
|
1751
|
+
{
|
|
1752
|
+
"id": "cons-08",
|
|
1753
|
+
"type": "write",
|
|
1754
|
+
"bloom": "apply",
|
|
1755
|
+
"prompt": "The interviewer asks: \"You replicate asynchronously. What happens if the leader dies?\" Answer in three or four sentences.",
|
|
1756
|
+
"key_points": [
|
|
1757
|
+
"Writes confirmed to the client but not yet copied can be lost",
|
|
1758
|
+
"A replica is promoted to leader, and it may be slightly behind",
|
|
1759
|
+
"Says which features can live with that, such as the feed or counters",
|
|
1760
|
+
"For money, a synchronous replica, accepting slower writes"
|
|
1761
|
+
],
|
|
1762
|
+
"model": "A replica gets promoted. Anything the old leader confirmed but hadn't copied yet is lost, so the new leader may be a few writes behind. For the feed and counters that's fine. For balances and invoices I'd keep one synchronous replica, so a confirmed write always exists twice, and accept the slower write.",
|
|
1763
|
+
"explain": "Asynchronous replication buys fast writes by confirming before the copy exists, so a failover can drop the newest writes. Naming the loss and where it's acceptable shows you know the trade. Say the risk, the feature that tolerates it, and the fix for the one that doesn't.",
|
|
1764
|
+
"source": "https://github.com/donnemartin/system-design-primer"
|
|
1765
|
+
},
|
|
1766
|
+
{
|
|
1767
|
+
"id": "cons-09",
|
|
1768
|
+
"type": "lightning",
|
|
1769
|
+
"bloom": "understand",
|
|
1770
|
+
"prompt": "When you hear this, reach for what? 90 seconds.",
|
|
1771
|
+
"pairs": [
|
|
1772
|
+
[
|
|
1773
|
+
"The network splits and the data must stay correct",
|
|
1774
|
+
"Favour consistency: refuse or wait"
|
|
1775
|
+
],
|
|
1776
|
+
[
|
|
1777
|
+
"The network splits and the page must stay up",
|
|
1778
|
+
"Favour availability: serve possibly stale data"
|
|
1779
|
+
],
|
|
1780
|
+
[
|
|
1781
|
+
"No split, but every millisecond counts",
|
|
1782
|
+
"PACELC: trade some consistency for latency"
|
|
1783
|
+
],
|
|
1784
|
+
[
|
|
1785
|
+
"A user must see their own edit on reload",
|
|
1786
|
+
"Read-your-writes"
|
|
1787
|
+
],
|
|
1788
|
+
[
|
|
1789
|
+
"A confirmed write must survive losing the leader",
|
|
1790
|
+
"Synchronous replication"
|
|
1791
|
+
],
|
|
1792
|
+
[
|
|
1793
|
+
"Writes must be fast and a little lag is fine",
|
|
1794
|
+
"Asynchronous replication"
|
|
1795
|
+
],
|
|
1796
|
+
[
|
|
1797
|
+
"Reads must overlap the latest write across replicas",
|
|
1798
|
+
"A quorum: R + W > N"
|
|
1799
|
+
],
|
|
1800
|
+
[
|
|
1801
|
+
"Like counts, view counters, a feed",
|
|
1802
|
+
"Eventual consistency"
|
|
1803
|
+
],
|
|
1804
|
+
[
|
|
1805
|
+
"An account balance or an invoice total",
|
|
1806
|
+
"Strong consistency"
|
|
1807
|
+
]
|
|
1808
|
+
],
|
|
1809
|
+
"explain": "Each pairing is a trade you should name without stopping to think, which leaves your time for the follow-up question. If one was slow, say that pairing out loud, with what it costs, before the next round.",
|
|
1810
|
+
"source": "https://www.hellointerview.com/learn/system-design/core-concepts/cap-theorem"
|
|
1811
|
+
},
|
|
1812
|
+
{
|
|
1813
|
+
"id": "cons-boss",
|
|
1814
|
+
"type": "sticker",
|
|
1815
|
+
"bloom": "evaluate",
|
|
1816
|
+
"prompt": "A team workspace product. Sticker each feature with what it needs; a box can take more than one sticker. Ask of each one: what breaks if this read is a few seconds old?",
|
|
1817
|
+
"diagram": {
|
|
1818
|
+
"width": 640,
|
|
1819
|
+
"height": 300,
|
|
1820
|
+
"nodes": [
|
|
1821
|
+
{
|
|
1822
|
+
"id": "app",
|
|
1823
|
+
"label": "Workspace app",
|
|
1824
|
+
"x": 260,
|
|
1825
|
+
"y": 126
|
|
1826
|
+
},
|
|
1827
|
+
{
|
|
1828
|
+
"id": "balance",
|
|
1829
|
+
"label": "Credit balance",
|
|
1830
|
+
"x": 20,
|
|
1831
|
+
"y": 20
|
|
1832
|
+
},
|
|
1833
|
+
{
|
|
1834
|
+
"id": "invoice",
|
|
1835
|
+
"label": "Invoice total",
|
|
1836
|
+
"x": 20,
|
|
1837
|
+
"y": 126
|
|
1838
|
+
},
|
|
1839
|
+
{
|
|
1840
|
+
"id": "likes",
|
|
1841
|
+
"label": "Like count",
|
|
1842
|
+
"x": 20,
|
|
1843
|
+
"y": 232
|
|
1844
|
+
},
|
|
1845
|
+
{
|
|
1846
|
+
"id": "views",
|
|
1847
|
+
"label": "Doc view counter",
|
|
1848
|
+
"x": 500,
|
|
1849
|
+
"y": 20
|
|
1850
|
+
},
|
|
1851
|
+
{
|
|
1852
|
+
"id": "feed",
|
|
1853
|
+
"label": "Activity feed",
|
|
1854
|
+
"x": 500,
|
|
1855
|
+
"y": 126
|
|
1856
|
+
},
|
|
1857
|
+
{
|
|
1858
|
+
"id": "profile",
|
|
1859
|
+
"label": "Edit own profile",
|
|
1860
|
+
"x": 500,
|
|
1861
|
+
"y": 232
|
|
1862
|
+
},
|
|
1863
|
+
{
|
|
1864
|
+
"id": "search",
|
|
1865
|
+
"label": "Search results",
|
|
1866
|
+
"x": 260,
|
|
1867
|
+
"y": 232
|
|
1868
|
+
}
|
|
1869
|
+
],
|
|
1870
|
+
"edges": [
|
|
1871
|
+
[
|
|
1872
|
+
"app",
|
|
1873
|
+
"balance"
|
|
1874
|
+
],
|
|
1875
|
+
[
|
|
1876
|
+
"app",
|
|
1877
|
+
"invoice"
|
|
1878
|
+
],
|
|
1879
|
+
[
|
|
1880
|
+
"app",
|
|
1881
|
+
"likes"
|
|
1882
|
+
],
|
|
1883
|
+
[
|
|
1884
|
+
"app",
|
|
1885
|
+
"views"
|
|
1886
|
+
],
|
|
1887
|
+
[
|
|
1888
|
+
"app",
|
|
1889
|
+
"feed"
|
|
1890
|
+
],
|
|
1891
|
+
[
|
|
1892
|
+
"app",
|
|
1893
|
+
"profile"
|
|
1894
|
+
],
|
|
1895
|
+
[
|
|
1896
|
+
"app",
|
|
1897
|
+
"search"
|
|
1898
|
+
]
|
|
1899
|
+
]
|
|
1900
|
+
},
|
|
1901
|
+
"stickers": [
|
|
1902
|
+
"Strong consistency",
|
|
1903
|
+
"Eventual is fine",
|
|
1904
|
+
"Read-your-writes"
|
|
1905
|
+
],
|
|
1906
|
+
"answers": {
|
|
1907
|
+
"balance": [
|
|
1908
|
+
"Strong consistency"
|
|
1909
|
+
],
|
|
1910
|
+
"invoice": [
|
|
1911
|
+
"Strong consistency"
|
|
1912
|
+
],
|
|
1913
|
+
"likes": [
|
|
1914
|
+
"Eventual is fine"
|
|
1915
|
+
],
|
|
1916
|
+
"views": [
|
|
1917
|
+
"Eventual is fine"
|
|
1918
|
+
],
|
|
1919
|
+
"feed": [
|
|
1920
|
+
"Eventual is fine"
|
|
1921
|
+
],
|
|
1922
|
+
"profile": [
|
|
1923
|
+
"Read-your-writes",
|
|
1924
|
+
"Eventual is fine"
|
|
1925
|
+
],
|
|
1926
|
+
"search": [
|
|
1927
|
+
"Eventual is fine"
|
|
1928
|
+
]
|
|
1929
|
+
},
|
|
1930
|
+
"explain": "Money and totals need strong consistency: a stale balance can show credit that's already gone, and an invoice total must match its lines. Counters, feeds and search can lag seconds harmlessly, so they stay fast and available. The profile needs read-your-writes for its editor only. Say: \"Strong where a stale read costs money; eventual everywhere else.\"",
|
|
1931
|
+
"source": "https://www.hellointerview.com/learn/system-design/core-concepts/cap-theorem"
|
|
1932
|
+
}
|
|
1933
|
+
]
|
|
1934
|
+
},
|
|
1935
|
+
{
|
|
1936
|
+
"chapter": 7,
|
|
1937
|
+
"slug": "async-failure",
|
|
1938
|
+
"title": "Async and failure",
|
|
1939
|
+
"emoji": "📮",
|
|
1940
|
+
"pillar": "Core concepts",
|
|
1941
|
+
"blurb": "Move slow work off the request path, and design for the retries, duplicates and slow partners that will happen.",
|
|
1942
|
+
"boss": "async-boss",
|
|
1943
|
+
"challenges": [
|
|
1944
|
+
{
|
|
1945
|
+
"id": "async-01",
|
|
1946
|
+
"type": "multi",
|
|
1947
|
+
"bloom": "understand",
|
|
1948
|
+
"prompt": "A user clicks \"Export my data\". Which are good reasons to put the export on a queue and return a job identifier (ID) straight away?",
|
|
1949
|
+
"options": [
|
|
1950
|
+
"Traffic spikes wait in the queue instead of overwhelming the servers",
|
|
1951
|
+
"The export takes minutes, far longer than a request should be held open",
|
|
1952
|
+
"A failed export can be retried without the user waiting on it",
|
|
1953
|
+
"It makes the export itself finish sooner",
|
|
1954
|
+
"It guarantees each export runs exactly once",
|
|
1955
|
+
"Web servers and workers can be scaled separately"
|
|
1956
|
+
],
|
|
1957
|
+
"correct": [
|
|
1958
|
+
0,
|
|
1959
|
+
1,
|
|
1960
|
+
2,
|
|
1961
|
+
5
|
|
1962
|
+
],
|
|
1963
|
+
"explain": "A queue splits \"accepted\" from \"done\": the request returns at once, workers drain the backlog at their own pace, and failures retry out of sight. It adds delay and possible duplicates; it doesn't remove them. Say: \"Accept, return a job ID, process in the background.\" For cheap, instant work, stay synchronous.",
|
|
1964
|
+
"source": "https://github.com/donnemartin/system-design-primer"
|
|
1965
|
+
},
|
|
1966
|
+
{
|
|
1967
|
+
"id": "async-02",
|
|
1968
|
+
"type": "duel",
|
|
1969
|
+
"bloom": "evaluate",
|
|
1970
|
+
"prompt": "Every \"invoice created\" event must reach the email worker, the analytics warehouse and the search indexer. Next quarter a new team wants to reprocess last week's events. A queue (like Amazon Simple Queue Service, SQS) or a log (like Kafka)?",
|
|
1971
|
+
"choices": [
|
|
1972
|
+
"A queue, like SQS: one worker takes each message, then it's deleted",
|
|
1973
|
+
"A log, like Kafka: events are kept, and each consumer group reads at its own pace"
|
|
1974
|
+
],
|
|
1975
|
+
"correct": 1,
|
|
1976
|
+
"reasons": [
|
|
1977
|
+
"Each consumer group gets every event, and kept events can be replayed",
|
|
1978
|
+
"A log never hands out an event twice",
|
|
1979
|
+
"A queue can only have one worker"
|
|
1980
|
+
],
|
|
1981
|
+
"reason": 0,
|
|
1982
|
+
"costs": [
|
|
1983
|
+
"More to run: partitions, retention and consumer offsets to manage",
|
|
1984
|
+
"Events vanish once the first consumer reads them",
|
|
1985
|
+
"Nothing extra"
|
|
1986
|
+
],
|
|
1987
|
+
"cost": 0,
|
|
1988
|
+
"explain": "A queue gives each message to one worker and deletes it once done: ideal for jobs. A log keeps events for a set period, so three consumer groups each see every event and a new one can start from last week. Say: \"Jobs go on a queue; facts that many readers need go on a log.\"",
|
|
1989
|
+
"source": "https://www.hellointerview.com/learn/system-design/deep-dives/kafka"
|
|
1990
|
+
},
|
|
1991
|
+
{
|
|
1992
|
+
"id": "async-03",
|
|
1993
|
+
"type": "recall",
|
|
1994
|
+
"bloom": "understand",
|
|
1995
|
+
"prompt": "Setting up a new customer touches three services, each with its own database. Why reach for a saga rather than two-phase commit (2PC)?",
|
|
1996
|
+
"answer": "2PC makes every service wait on one coordinator, holding its resources, and a coordinator failure can leave them stuck. A saga runs each local step in turn and, if one fails, undoes the finished steps with compensating actions.",
|
|
1997
|
+
"explain": "Across services with their own databases, 2PC is slow, blocks on its coordinator, and often isn't on offer at all. A saga gives up isolation: in-between states are visible and the result is eventually consistent, so every step and every undo must be safe to retry. Say: \"A saga with compensating steps, run by an orchestrator.\"",
|
|
1998
|
+
"source": "https://docs.aws.amazon.com/prescriptive-guidance/latest/cloud-design-patterns/saga-orchestration.html"
|
|
1999
|
+
},
|
|
2000
|
+
{
|
|
2001
|
+
"id": "async-04",
|
|
2002
|
+
"type": "fill",
|
|
2003
|
+
"bloom": "apply",
|
|
2004
|
+
"prompt": "Fill the boxes. The export API (application programming interface) returns a job ID at once, the export runs in the background, and a job that keeps failing is set aside for a person to inspect, not retried for ever.",
|
|
2005
|
+
"diagram": {
|
|
2006
|
+
"width": 640,
|
|
2007
|
+
"height": 300,
|
|
2008
|
+
"nodes": [
|
|
2009
|
+
{
|
|
2010
|
+
"id": "client",
|
|
2011
|
+
"label": "Client",
|
|
2012
|
+
"x": 20,
|
|
2013
|
+
"y": 126
|
|
2014
|
+
},
|
|
2015
|
+
{
|
|
2016
|
+
"id": "api",
|
|
2017
|
+
"label": "Export API",
|
|
2018
|
+
"x": 170,
|
|
2019
|
+
"y": 126
|
|
2020
|
+
},
|
|
2021
|
+
{
|
|
2022
|
+
"id": "jobs",
|
|
2023
|
+
"label": "Job status table",
|
|
2024
|
+
"x": 170,
|
|
2025
|
+
"y": 20
|
|
2026
|
+
},
|
|
2027
|
+
{
|
|
2028
|
+
"id": "s-queue",
|
|
2029
|
+
"label": "?",
|
|
2030
|
+
"x": 330,
|
|
2031
|
+
"y": 126
|
|
2032
|
+
},
|
|
2033
|
+
{
|
|
2034
|
+
"id": "s-workers",
|
|
2035
|
+
"label": "?",
|
|
2036
|
+
"x": 500,
|
|
2037
|
+
"y": 126
|
|
2038
|
+
},
|
|
2039
|
+
{
|
|
2040
|
+
"id": "s-dlq",
|
|
2041
|
+
"label": "?",
|
|
2042
|
+
"x": 330,
|
|
2043
|
+
"y": 232
|
|
2044
|
+
},
|
|
2045
|
+
{
|
|
2046
|
+
"id": "files",
|
|
2047
|
+
"label": "File store",
|
|
2048
|
+
"x": 500,
|
|
2049
|
+
"y": 232
|
|
2050
|
+
}
|
|
2051
|
+
],
|
|
2052
|
+
"edges": [
|
|
2053
|
+
[
|
|
2054
|
+
"client",
|
|
2055
|
+
"api",
|
|
2056
|
+
"start export"
|
|
2057
|
+
],
|
|
2058
|
+
[
|
|
2059
|
+
"api",
|
|
2060
|
+
"jobs",
|
|
2061
|
+
"job ID, pending"
|
|
2062
|
+
],
|
|
2063
|
+
[
|
|
2064
|
+
"api",
|
|
2065
|
+
"s-queue",
|
|
2066
|
+
"job"
|
|
2067
|
+
],
|
|
2068
|
+
[
|
|
2069
|
+
"s-queue",
|
|
2070
|
+
"s-workers"
|
|
2071
|
+
],
|
|
2072
|
+
[
|
|
2073
|
+
"s-workers",
|
|
2074
|
+
"files",
|
|
2075
|
+
"writes the file"
|
|
2076
|
+
],
|
|
2077
|
+
[
|
|
2078
|
+
"s-workers",
|
|
2079
|
+
"jobs",
|
|
2080
|
+
"marks done"
|
|
2081
|
+
],
|
|
2082
|
+
[
|
|
2083
|
+
"s-queue",
|
|
2084
|
+
"s-dlq",
|
|
2085
|
+
"after 5 failed tries"
|
|
2086
|
+
]
|
|
2087
|
+
]
|
|
2088
|
+
},
|
|
2089
|
+
"slots": {
|
|
2090
|
+
"s-queue": "Queue",
|
|
2091
|
+
"s-workers": "Worker pool",
|
|
2092
|
+
"s-dlq": "Dead-letter queue"
|
|
2093
|
+
},
|
|
2094
|
+
"palette": [
|
|
2095
|
+
"Queue",
|
|
2096
|
+
"Worker pool",
|
|
2097
|
+
"Dead-letter queue",
|
|
2098
|
+
"Cache",
|
|
2099
|
+
"Load balancer",
|
|
2100
|
+
"Circuit breaker"
|
|
2101
|
+
],
|
|
2102
|
+
"explain": "The queue absorbs bursts and the worker pool scales on its own. A message that fails every time (a poison message) would otherwise loop for ever, so after a set number of tries it moves to the dead-letter queue (DLQ). Say: \"Retries for blips, a DLQ with an alarm for the rest.\"",
|
|
2103
|
+
"source": "https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-dead-letter-queues.html"
|
|
2104
|
+
},
|
|
2105
|
+
{
|
|
2106
|
+
"id": "async-05",
|
|
2107
|
+
"type": "order",
|
|
2108
|
+
"bloom": "apply",
|
|
2109
|
+
"prompt": "A worker's call to an outside partner fails. Put a sensible retry policy in order.",
|
|
2110
|
+
"items": [
|
|
2111
|
+
"The call hits its timeout or fails",
|
|
2112
|
+
"Retry after a short wait",
|
|
2113
|
+
"Retry again, doubling the wait and adding random jitter",
|
|
2114
|
+
"Give up after a capped number of tries",
|
|
2115
|
+
"Move the message to the dead-letter queue",
|
|
2116
|
+
"Alert, fix the cause, then send the messages back"
|
|
2117
|
+
],
|
|
2118
|
+
"explain": "A timeout stops a slow call holding a worker. Backoff gives the partner room to recover, jitter stops every client retrying in the same instant, and the cap surfaces the failure. Retry at one layer only, or the retries multiply. Say: \"Capped exponential backoff with jitter, then the DLQ.\"",
|
|
2119
|
+
"source": "https://aws.amazon.com/builders-library/timeouts-retries-and-backoff-with-jitter/"
|
|
2120
|
+
},
|
|
2121
|
+
{
|
|
2122
|
+
"id": "async-06",
|
|
2123
|
+
"type": "multi",
|
|
2124
|
+
"bloom": "analyse",
|
|
2125
|
+
"prompt": "The queue delivers at least once, so any message may be processed twice. Which of these consumers are already safe to run twice?",
|
|
2126
|
+
"options": [
|
|
2127
|
+
"Resize an image and save it under a name made from the image's ID",
|
|
2128
|
+
"Set an invoice's status to paid",
|
|
2129
|
+
"Add the payment's amount to the account balance",
|
|
2130
|
+
"Send the invoice email",
|
|
2131
|
+
"Append an audit row with a freshly generated ID",
|
|
2132
|
+
"Build the monthly report and overwrite that month's file"
|
|
2133
|
+
],
|
|
2134
|
+
"correct": [
|
|
2135
|
+
0,
|
|
2136
|
+
1,
|
|
2137
|
+
5
|
|
2138
|
+
],
|
|
2139
|
+
"explain": "Setting a value or overwriting a fixed name gives the same result however often it runs. Adding, sending and appending don't. Make those safe with an idempotency key: record the message's ID in the same transaction as the change, and skip IDs already seen. Say: \"At-least-once delivery, so every consumer is idempotent.\"",
|
|
2140
|
+
"source": "https://aws.amazon.com/builders-library/making-retries-safe-with-idempotent-apis/"
|
|
2141
|
+
},
|
|
2142
|
+
{
|
|
2143
|
+
"id": "async-07",
|
|
2144
|
+
"type": "duel",
|
|
2145
|
+
"bloom": "evaluate",
|
|
2146
|
+
"prompt": "Every invoice calls an outside tax-rate service. It starts taking 30 seconds, then failing. Your servers' threads pile up waiting and the whole app slows down. Best move?",
|
|
2147
|
+
"choices": [
|
|
2148
|
+
"Retry each failed call three times straight away",
|
|
2149
|
+
"A short timeout and a circuit breaker, with a fallback rate",
|
|
2150
|
+
"Raise the timeout to 60 seconds so calls can finish"
|
|
2151
|
+
],
|
|
2152
|
+
"correct": 1,
|
|
2153
|
+
"reasons": [
|
|
2154
|
+
"After repeated failures it fails fast, freeing threads and letting the partner recover",
|
|
2155
|
+
"It makes the partner respond faster",
|
|
2156
|
+
"It guarantees no invoice ever fails"
|
|
2157
|
+
],
|
|
2158
|
+
"reason": 0,
|
|
2159
|
+
"costs": [
|
|
2160
|
+
"While it's open you need a fallback: a cached rate, or \"try again later\"",
|
|
2161
|
+
"It doubles the load on the partner",
|
|
2162
|
+
"Nothing; it's free"
|
|
2163
|
+
],
|
|
2164
|
+
"cost": 0,
|
|
2165
|
+
"explain": "Waiting on a dying partner ties up threads until your service fails too. A breaker counts failures, trips, and returns an error at once; after a pause it lets one trial call through. Instant retries pile more load on a struggling service. Say: \"Timeout, breaker, fallback, so their outage stays theirs.\"",
|
|
2166
|
+
"source": "https://martinfowler.com/bliki/CircuitBreaker.html"
|
|
2167
|
+
},
|
|
2168
|
+
{
|
|
2169
|
+
"id": "async-08",
|
|
2170
|
+
"type": "write",
|
|
2171
|
+
"bloom": "apply",
|
|
2172
|
+
"prompt": "The invoice service saves an invoice, then publishes \"invoice created\" to the message broker. Sometimes the save works and the publish doesn't. Explain your fix to the interviewer in three or four sentences.",
|
|
2173
|
+
"key_points": [
|
|
2174
|
+
"Names the problem: two systems written separately, so one can succeed alone",
|
|
2175
|
+
"Writes the invoice and an outbox row in one database transaction",
|
|
2176
|
+
"A relay reads the outbox and publishes, retrying until the broker accepts",
|
|
2177
|
+
"The relay can publish twice, so consumers are idempotent"
|
|
2178
|
+
],
|
|
2179
|
+
"model": "Saving and publishing are two separate writes, so a crash between them loses the event. I'd write the invoice and an outbox row in the same database transaction, so both commit or neither does. A relay reads new outbox rows, publishes them and marks them sent. After a crash it can publish a row twice, so consumers skip event IDs they've already seen.",
|
|
2180
|
+
"explain": "The dual write is the bug: no transaction spans a database and a broker. The outbox puts the event inside the transaction you already have, and the relay makes publishing at-least-once, never zero. Say: \"Outbox table, relay, idempotent consumers.\"",
|
|
2181
|
+
"source": "https://docs.aws.amazon.com/prescriptive-guidance/latest/cloud-design-patterns/transactional-outbox.html"
|
|
2182
|
+
},
|
|
2183
|
+
{
|
|
2184
|
+
"id": "async-09",
|
|
2185
|
+
"type": "lightning",
|
|
2186
|
+
"bloom": "understand",
|
|
2187
|
+
"prompt": "When you hear this, reach for what? 90 seconds.",
|
|
2188
|
+
"pairs": [
|
|
2189
|
+
[
|
|
2190
|
+
"Traffic bursts the workers can't keep up with",
|
|
2191
|
+
"A queue to absorb the burst"
|
|
2192
|
+
],
|
|
2193
|
+
[
|
|
2194
|
+
"The same message may arrive twice",
|
|
2195
|
+
"An idempotent consumer"
|
|
2196
|
+
],
|
|
2197
|
+
[
|
|
2198
|
+
"One message fails every single time",
|
|
2199
|
+
"A dead-letter queue"
|
|
2200
|
+
],
|
|
2201
|
+
[
|
|
2202
|
+
"Thousands of clients retry at the same instant",
|
|
2203
|
+
"Exponential backoff with jitter"
|
|
2204
|
+
],
|
|
2205
|
+
[
|
|
2206
|
+
"A partner keeps failing and threads pile up",
|
|
2207
|
+
"A circuit breaker"
|
|
2208
|
+
],
|
|
2209
|
+
[
|
|
2210
|
+
"The queue grows faster than it drains, for hours",
|
|
2211
|
+
"Back pressure: cap the queue, push back"
|
|
2212
|
+
],
|
|
2213
|
+
[
|
|
2214
|
+
"Save a record and publish an event, both or neither",
|
|
2215
|
+
"The outbox pattern"
|
|
2216
|
+
],
|
|
2217
|
+
[
|
|
2218
|
+
"Steps across services, each undoable if a later one fails",
|
|
2219
|
+
"A saga with compensating steps"
|
|
2220
|
+
],
|
|
2221
|
+
[
|
|
2222
|
+
"A job that runs for days and must survive restarts",
|
|
2223
|
+
"A workflow engine, like Temporal"
|
|
2224
|
+
],
|
|
2225
|
+
[
|
|
2226
|
+
"Every call to a partner needs a limit on waiting",
|
|
2227
|
+
"A timeout"
|
|
2228
|
+
]
|
|
2229
|
+
],
|
|
2230
|
+
"explain": "Each failure has a standard answer, and naming it fast leaves your time for the trade-off. If one was slow, say that pairing out loud, with what it costs, before the next round.",
|
|
2231
|
+
"source": "https://github.com/donnemartin/system-design-primer"
|
|
2232
|
+
},
|
|
2233
|
+
{
|
|
2234
|
+
"id": "async-boss",
|
|
2235
|
+
"type": "flaw",
|
|
2236
|
+
"bloom": "analyse",
|
|
2237
|
+
"prompt": "This flow credits customers' accounts from the payment provider's webhooks (calls the provider makes to you). The provider retries until it gets a success response, and the queue delivers at least once. Find the weak box, then pick the fix.",
|
|
2238
|
+
"diagram": {
|
|
2239
|
+
"width": 640,
|
|
2240
|
+
"height": 300,
|
|
2241
|
+
"nodes": [
|
|
2242
|
+
{
|
|
2243
|
+
"id": "provider",
|
|
2244
|
+
"label": "Payment provider",
|
|
2245
|
+
"x": 10,
|
|
2246
|
+
"y": 126
|
|
2247
|
+
},
|
|
2248
|
+
{
|
|
2249
|
+
"id": "hook",
|
|
2250
|
+
"label": "Webhook: verify, queue, 200",
|
|
2251
|
+
"x": 150,
|
|
2252
|
+
"y": 126,
|
|
2253
|
+
"w": 150
|
|
2254
|
+
},
|
|
2255
|
+
{
|
|
2256
|
+
"id": "queue",
|
|
2257
|
+
"label": "Queue",
|
|
2258
|
+
"x": 330,
|
|
2259
|
+
"y": 126
|
|
2260
|
+
},
|
|
2261
|
+
{
|
|
2262
|
+
"id": "dlq",
|
|
2263
|
+
"label": "Dead-letter queue",
|
|
2264
|
+
"x": 330,
|
|
2265
|
+
"y": 232
|
|
2266
|
+
},
|
|
2267
|
+
{
|
|
2268
|
+
"id": "worker",
|
|
2269
|
+
"label": "Worker adds amount to balance",
|
|
2270
|
+
"x": 480,
|
|
2271
|
+
"y": 126,
|
|
2272
|
+
"w": 150
|
|
2273
|
+
},
|
|
2274
|
+
{
|
|
2275
|
+
"id": "db",
|
|
2276
|
+
"label": "Accounts database",
|
|
2277
|
+
"x": 480,
|
|
2278
|
+
"y": 232,
|
|
2279
|
+
"w": 150
|
|
2280
|
+
}
|
|
2281
|
+
],
|
|
2282
|
+
"edges": [
|
|
2283
|
+
[
|
|
2284
|
+
"provider",
|
|
2285
|
+
"hook"
|
|
2286
|
+
],
|
|
2287
|
+
[
|
|
2288
|
+
"hook",
|
|
2289
|
+
"queue"
|
|
2290
|
+
],
|
|
2291
|
+
[
|
|
2292
|
+
"queue",
|
|
2293
|
+
"worker"
|
|
2294
|
+
],
|
|
2295
|
+
[
|
|
2296
|
+
"queue",
|
|
2297
|
+
"dlq",
|
|
2298
|
+
"after 5 tries"
|
|
2299
|
+
],
|
|
2300
|
+
[
|
|
2301
|
+
"worker",
|
|
2302
|
+
"db",
|
|
2303
|
+
"balance += amount"
|
|
2304
|
+
]
|
|
2305
|
+
]
|
|
2306
|
+
},
|
|
2307
|
+
"flaw": "worker",
|
|
2308
|
+
"fixes": [
|
|
2309
|
+
"Record each event's ID with the balance change in one transaction; skip IDs already seen",
|
|
2310
|
+
"Add more workers so events are processed faster",
|
|
2311
|
+
"Have the webhook update the balance itself before replying 200",
|
|
2312
|
+
"Remove the dead-letter queue so no event is set aside",
|
|
2313
|
+
"Move the events to a log so they're kept for a week"
|
|
2314
|
+
],
|
|
2315
|
+
"fix": 0,
|
|
2316
|
+
"explain": "Duplicates are certain here: the provider retries and the queue redelivers. \"Add the amount\" applies every copy, so one payment can credit twice. The provider's event ID is a ready-made idempotency key: store it in the same transaction as the credit, with a unique constraint, and skip repeats. Say: \"Skip repeats by event ID, atomically with the write.\"",
|
|
2317
|
+
"source": "https://aws.amazon.com/builders-library/making-retries-safe-with-idempotent-apis/"
|
|
2318
|
+
}
|
|
2319
|
+
]
|
|
2320
|
+
},
|
|
2321
|
+
{
|
|
2322
|
+
"chapter": 8,
|
|
2323
|
+
"slug": "patterns",
|
|
2324
|
+
"title": "Patterns",
|
|
2325
|
+
"emoji": "🧱",
|
|
2326
|
+
"pillar": "Patterns",
|
|
2327
|
+
"blurb": "The moves that recur in most designs. Spot which one a problem needs, then say what it costs.",
|
|
2328
|
+
"boss": "pat-boss",
|
|
2329
|
+
"challenges": [
|
|
2330
|
+
{
|
|
2331
|
+
"id": "pat-01",
|
|
2332
|
+
"type": "duel",
|
|
2333
|
+
"bloom": "evaluate",
|
|
2334
|
+
"prompt": "A team dashboard's main query joins six tables and takes 2 seconds. It's read 200 times a minute, and the data under it changes all day. What do you do?",
|
|
2335
|
+
"choices": [
|
|
2336
|
+
"Keep a precomputed read model, updated as the data changes",
|
|
2337
|
+
"Cache the query result for an hour",
|
|
2338
|
+
"Add a read replica and run the same query there"
|
|
2339
|
+
],
|
|
2340
|
+
"correct": 0,
|
|
2341
|
+
"reasons": [
|
|
2342
|
+
"Readers get a ready-made row in milliseconds, and it stays close to current",
|
|
2343
|
+
"A cache is never stale",
|
|
2344
|
+
"A replica runs the same join faster"
|
|
2345
|
+
],
|
|
2346
|
+
"reason": 0,
|
|
2347
|
+
"costs": [
|
|
2348
|
+
"Two shapes of the data to keep in step, and the read model lags slightly",
|
|
2349
|
+
"The dashboard can't be filtered any more",
|
|
2350
|
+
"Writes stop working"
|
|
2351
|
+
],
|
|
2352
|
+
"cost": 0,
|
|
2353
|
+
"explain": "When a read is expensive and constant, do the work once at write time. A separate read model (the idea behind CQRS, command query responsibility segregation) is shaped for the screen, while a cache would be an hour stale and a replica still runs the slow join. Say: \"Precompute the dashboard's view as data changes; accept a second or two of lag.\"",
|
|
2354
|
+
"source": "https://martinfowler.com/bliki/CQRS.html"
|
|
2355
|
+
},
|
|
2356
|
+
{
|
|
2357
|
+
"id": "pat-02",
|
|
2358
|
+
"type": "multi",
|
|
2359
|
+
"bloom": "analyse",
|
|
2360
|
+
"prompt": "A metrics service takes 200,000 small writes a second, and one database can't keep up. Which of these help it scale writes?",
|
|
2361
|
+
"options": [
|
|
2362
|
+
"Buffer writes in memory and flush them in batches every second",
|
|
2363
|
+
"Split the data across machines by a well-spread key",
|
|
2364
|
+
"Use a store built for heavy writes, such as Cassandra",
|
|
2365
|
+
"Add up counts before storing them, one row a minute per metric",
|
|
2366
|
+
"Add read replicas",
|
|
2367
|
+
"Put a cache in front of the database"
|
|
2368
|
+
],
|
|
2369
|
+
"correct": [
|
|
2370
|
+
0,
|
|
2371
|
+
1,
|
|
2372
|
+
2,
|
|
2373
|
+
3
|
|
2374
|
+
],
|
|
2375
|
+
"explain": "Writes scale by doing fewer, cheaper or spread-out writes: batch them, pre-aggregate them, shard them, or use a store built for appends. Replicas copy every write, so they add work, and a cache helps reads, not writes. Say: \"Batch and pre-aggregate first, then shard by metric if it's still too much.\"",
|
|
2376
|
+
"source": "https://www.hellointerview.com/learn/system-design/in-a-hurry/patterns"
|
|
2377
|
+
},
|
|
2378
|
+
{
|
|
2379
|
+
"id": "pat-03",
|
|
2380
|
+
"type": "order",
|
|
2381
|
+
"bloom": "apply",
|
|
2382
|
+
"prompt": "A user uploads a 2 GB video from a phone on a patchy connection. Put the steps in order.",
|
|
2383
|
+
"items": [
|
|
2384
|
+
"The app asks the server to start an upload; the server saves a pending record",
|
|
2385
|
+
"The server returns presigned URLs, one for each part",
|
|
2386
|
+
"The app sends the parts straight to object storage, retrying any part that fails",
|
|
2387
|
+
"The app tells the server it has finished",
|
|
2388
|
+
"The server checks the parts and marks the video uploaded",
|
|
2389
|
+
"Workers process the video, such as transcoding it"
|
|
2390
|
+
],
|
|
2391
|
+
"explain": "Big files go straight from the client to object storage in parts, so your servers never carry the bytes and a dropped connection costs one part, not the whole file. The database keeps only the record and its state. Say: \"Multipart, presigned uploads straight to storage, with a pending record until it completes.\"",
|
|
2392
|
+
"source": "https://docs.aws.amazon.com/AmazonS3/latest/userguide/mpuoverview.html"
|
|
2393
|
+
},
|
|
2394
|
+
{
|
|
2395
|
+
"id": "pat-04",
|
|
2396
|
+
"type": "flaw",
|
|
2397
|
+
"bloom": "analyse",
|
|
2398
|
+
"prompt": "A video course site. Viewers around the world stream lessons. Find the box that will hurt first, then pick the fix.",
|
|
2399
|
+
"diagram": {
|
|
2400
|
+
"width": 640,
|
|
2401
|
+
"height": 300,
|
|
2402
|
+
"nodes": [
|
|
2403
|
+
{
|
|
2404
|
+
"id": "viewers",
|
|
2405
|
+
"label": "Viewers worldwide",
|
|
2406
|
+
"x": 20,
|
|
2407
|
+
"y": 126
|
|
2408
|
+
},
|
|
2409
|
+
{
|
|
2410
|
+
"id": "lb",
|
|
2411
|
+
"label": "Load balancer",
|
|
2412
|
+
"x": 180,
|
|
2413
|
+
"y": 126
|
|
2414
|
+
},
|
|
2415
|
+
{
|
|
2416
|
+
"id": "app",
|
|
2417
|
+
"label": "App servers: stream video bytes",
|
|
2418
|
+
"x": 340,
|
|
2419
|
+
"y": 126,
|
|
2420
|
+
"w": 140
|
|
2421
|
+
},
|
|
2422
|
+
{
|
|
2423
|
+
"id": "s3",
|
|
2424
|
+
"label": "Object storage: videos",
|
|
2425
|
+
"x": 500,
|
|
2426
|
+
"y": 40,
|
|
2427
|
+
"w": 130
|
|
2428
|
+
},
|
|
2429
|
+
{
|
|
2430
|
+
"id": "db",
|
|
2431
|
+
"label": "Database: courses, progress",
|
|
2432
|
+
"x": 500,
|
|
2433
|
+
"y": 212,
|
|
2434
|
+
"w": 130
|
|
2435
|
+
}
|
|
2436
|
+
],
|
|
2437
|
+
"edges": [
|
|
2438
|
+
[
|
|
2439
|
+
"viewers",
|
|
2440
|
+
"lb"
|
|
2441
|
+
],
|
|
2442
|
+
[
|
|
2443
|
+
"lb",
|
|
2444
|
+
"app"
|
|
2445
|
+
],
|
|
2446
|
+
[
|
|
2447
|
+
"app",
|
|
2448
|
+
"s3",
|
|
2449
|
+
"reads whole files"
|
|
2450
|
+
],
|
|
2451
|
+
[
|
|
2452
|
+
"app",
|
|
2453
|
+
"db"
|
|
2454
|
+
]
|
|
2455
|
+
]
|
|
2456
|
+
},
|
|
2457
|
+
"flaw": "app",
|
|
2458
|
+
"fixes": [
|
|
2459
|
+
"Serve videos from a CDN in front of object storage, using signed links",
|
|
2460
|
+
"Add more app servers",
|
|
2461
|
+
"Keep popular videos in a Redis cache",
|
|
2462
|
+
"Store the videos in the database"
|
|
2463
|
+
],
|
|
2464
|
+
"fix": 0,
|
|
2465
|
+
"explain": "Pushing every byte of video through your own servers wastes them and puts far-away viewers on slow paths. A CDN (content delivery network) serves copies from near each viewer, and signed, short-lived links keep paid lessons private. Say: \"Videos from a CDN with signed links; the app only hands out the link.\"",
|
|
2466
|
+
"source": "https://www.hellointerview.com/learn/system-design/in-a-hurry/patterns"
|
|
2467
|
+
},
|
|
2468
|
+
{
|
|
2469
|
+
"id": "pat-05",
|
|
2470
|
+
"type": "write",
|
|
2471
|
+
"bloom": "apply",
|
|
2472
|
+
"prompt": "A year-end report takes about four minutes to build. Explain how the request works and how the user finds out it's ready.",
|
|
2473
|
+
"key_points": [
|
|
2474
|
+
"The request returns at once (202 Accepted) with a job ID",
|
|
2475
|
+
"The work goes on a queue for workers, not the web server",
|
|
2476
|
+
"A job record tracks its state, such as pending, running, done or failed",
|
|
2477
|
+
"The client checks a status endpoint, or gets a live update or an email when it's done",
|
|
2478
|
+
"The finished file is fetched by a link, such as a presigned URL"
|
|
2479
|
+
],
|
|
2480
|
+
"model": "POST /reports returns 202 and a job ID straight away. A worker picks the job off a queue and updates the job's state as it goes. The page checks GET /jobs/{id} every few seconds, or listens for a server-sent event; when it's done, the response includes a short-lived link to the file. A failed job shows as failed, with a retry button.",
|
|
2481
|
+
"explain": "Holding a request open for minutes ties up a server and breaks on any timeout or deploy. Accepting the job and reporting its state lets the work survive restarts and lets workers scale on their own. Say: \"Accept, return a job ID, process in the background, report the state.\"",
|
|
2482
|
+
"source": "https://www.hellointerview.com/learn/system-design/in-a-hurry/patterns"
|
|
2483
|
+
},
|
|
2484
|
+
{
|
|
2485
|
+
"id": "pat-06",
|
|
2486
|
+
"type": "duel",
|
|
2487
|
+
"bloom": "evaluate",
|
|
2488
|
+
"prompt": "Onboarding a customer: create the account, take the first payment, set up their workspace, then send a welcome email. Steps can fail or take minutes, and the process must survive a restart. How do you run it?",
|
|
2489
|
+
"choices": [
|
|
2490
|
+
"A workflow engine, such as Temporal or AWS Step Functions, running the steps in order",
|
|
2491
|
+
"All four steps inside one HTTP request",
|
|
2492
|
+
"Each service reacts to the previous one's event, with no central view"
|
|
2493
|
+
],
|
|
2494
|
+
"correct": 0,
|
|
2495
|
+
"reasons": [
|
|
2496
|
+
"It records each finished step, retries failures, and resumes after a crash",
|
|
2497
|
+
"It makes every step run faster",
|
|
2498
|
+
"It removes the need for idempotent steps"
|
|
2499
|
+
],
|
|
2500
|
+
"reason": 0,
|
|
2501
|
+
"costs": [
|
|
2502
|
+
"One more system to run, and steps must be safe to replay",
|
|
2503
|
+
"The customer waits for every step",
|
|
2504
|
+
"Nobody can see where a stuck customer is"
|
|
2505
|
+
],
|
|
2506
|
+
"cost": 0,
|
|
2507
|
+
"explain": "With more than two or three steps, a central orchestrator gives you one place to see progress, retry and undo, and it survives restarts. Chaining events between services works for short flows but hides where a stuck case is. One request breaks on the first slow step. Say: \"A workflow engine for the multi-step part; each step idempotent.\"",
|
|
2508
|
+
"source": "https://www.hellointerview.com/learn/system-design/deep-dives/temporal"
|
|
2509
|
+
},
|
|
2510
|
+
{
|
|
2511
|
+
"id": "pat-07",
|
|
2512
|
+
"type": "sticker",
|
|
2513
|
+
"bloom": "analyse",
|
|
2514
|
+
"prompt": "Live updates for a shared task board. Put each trait on the box it describes.",
|
|
2515
|
+
"diagram": {
|
|
2516
|
+
"width": 640,
|
|
2517
|
+
"height": 300,
|
|
2518
|
+
"nodes": [
|
|
2519
|
+
{
|
|
2520
|
+
"id": "browsers",
|
|
2521
|
+
"label": "Browsers",
|
|
2522
|
+
"x": 20,
|
|
2523
|
+
"y": 126
|
|
2524
|
+
},
|
|
2525
|
+
{
|
|
2526
|
+
"id": "conn",
|
|
2527
|
+
"label": "Connection servers",
|
|
2528
|
+
"x": 180,
|
|
2529
|
+
"y": 126
|
|
2530
|
+
},
|
|
2531
|
+
{
|
|
2532
|
+
"id": "pubsub",
|
|
2533
|
+
"label": "Pub/sub channel",
|
|
2534
|
+
"x": 340,
|
|
2535
|
+
"y": 20
|
|
2536
|
+
},
|
|
2537
|
+
{
|
|
2538
|
+
"id": "app",
|
|
2539
|
+
"label": "App servers",
|
|
2540
|
+
"x": 340,
|
|
2541
|
+
"y": 210
|
|
2542
|
+
},
|
|
2543
|
+
{
|
|
2544
|
+
"id": "db",
|
|
2545
|
+
"label": "Database",
|
|
2546
|
+
"x": 500,
|
|
2547
|
+
"y": 210
|
|
2548
|
+
}
|
|
2549
|
+
],
|
|
2550
|
+
"edges": [
|
|
2551
|
+
[
|
|
2552
|
+
"browsers",
|
|
2553
|
+
"conn",
|
|
2554
|
+
"SSE or WebSocket"
|
|
2555
|
+
],
|
|
2556
|
+
[
|
|
2557
|
+
"pubsub",
|
|
2558
|
+
"conn",
|
|
2559
|
+
"update"
|
|
2560
|
+
],
|
|
2561
|
+
[
|
|
2562
|
+
"app",
|
|
2563
|
+
"pubsub",
|
|
2564
|
+
"publish"
|
|
2565
|
+
],
|
|
2566
|
+
[
|
|
2567
|
+
"app",
|
|
2568
|
+
"db",
|
|
2569
|
+
"write"
|
|
2570
|
+
]
|
|
2571
|
+
]
|
|
2572
|
+
},
|
|
2573
|
+
"stickers": [
|
|
2574
|
+
"Holds open connections",
|
|
2575
|
+
"Needs care on deploys",
|
|
2576
|
+
"Sends one update to every subscriber",
|
|
2577
|
+
"The source of truth",
|
|
2578
|
+
"Any instance takes any request"
|
|
2579
|
+
],
|
|
2580
|
+
"answers": {
|
|
2581
|
+
"conn": [
|
|
2582
|
+
"Holds open connections",
|
|
2583
|
+
"Needs care on deploys"
|
|
2584
|
+
],
|
|
2585
|
+
"pubsub": [
|
|
2586
|
+
"Sends one update to every subscriber"
|
|
2587
|
+
],
|
|
2588
|
+
"db": [
|
|
2589
|
+
"The source of truth"
|
|
2590
|
+
],
|
|
2591
|
+
"app": [
|
|
2592
|
+
"Any instance takes any request"
|
|
2593
|
+
]
|
|
2594
|
+
},
|
|
2595
|
+
"explain": "A write lands on any app server, but the reader's connection lives on one of many connection servers. The app server saves to the database and publishes the change; every connection server that subscribed passes it to its own browsers. Connection servers hold state, so deploys must drain connections gently. Say: \"Write, publish, fan out to the connection servers.\"",
|
|
2596
|
+
"source": "https://redis.io/docs/latest/develop/interact/pubsub/"
|
|
2597
|
+
},
|
|
2598
|
+
{
|
|
2599
|
+
"id": "pat-08",
|
|
2600
|
+
"type": "recall",
|
|
2601
|
+
"bloom": "understand",
|
|
2602
|
+
"prompt": "Why does a live-updates design usually put a pub/sub layer between the app servers and the connection servers?",
|
|
2603
|
+
"answer": "The server that handles a write doesn't know which connection server each reader is on. It publishes once; every connection server subscribed to that channel forwards the update to its own readers.",
|
|
2604
|
+
"explain": "Without it, every app server would have to know every connection, which breaks as soon as there's more than one of each. Say: \"Pub/sub decouples who writes from who's connected.\"",
|
|
2605
|
+
"source": "https://redis.io/docs/latest/develop/interact/pubsub/"
|
|
2606
|
+
},
|
|
2607
|
+
{
|
|
2608
|
+
"id": "pat-09",
|
|
2609
|
+
"type": "duel",
|
|
2610
|
+
"bloom": "evaluate",
|
|
2611
|
+
"prompt": "Two fans tap \"Book\" on the last seat in the same second. Seats live in PostgreSQL. How do you make sure exactly one of them gets it?",
|
|
2612
|
+
"choices": [
|
|
2613
|
+
"One conditional update: claim the seat only if it's still free",
|
|
2614
|
+
"Read the seat, check it's free, then write in a second query",
|
|
2615
|
+
"Put a cache in front of the seats table"
|
|
2616
|
+
],
|
|
2617
|
+
"correct": 0,
|
|
2618
|
+
"reasons": [
|
|
2619
|
+
"Check and claim are one atomic step, so the later claim changes no rows",
|
|
2620
|
+
"It's faster than reading first",
|
|
2621
|
+
"The cache makes requests take turns"
|
|
2622
|
+
],
|
|
2623
|
+
"reason": 0,
|
|
2624
|
+
"costs": [
|
|
2625
|
+
"The loser needs a clear \"just taken\" message and a way to pick again",
|
|
2626
|
+
"The seat can still be sold twice",
|
|
2627
|
+
"Every booking waits on one global lock"
|
|
2628
|
+
],
|
|
2629
|
+
"cost": 0,
|
|
2630
|
+
"explain": "Reading, then writing, leaves a gap where both fans see \"free\". Let the database decide in one statement: set the holder WHERE the seat is still free, and treat zero rows changed as \"someone beat you\". A version number checked on write (optimistic locking) is the same idea. Say: \"Claim it with a conditional update; zero rows means it's gone.\"",
|
|
2631
|
+
"source": "https://www.postgresql.org/docs/current/transaction-iso.html"
|
|
2632
|
+
},
|
|
2633
|
+
{
|
|
2634
|
+
"id": "pat-10",
|
|
2635
|
+
"type": "duel",
|
|
2636
|
+
"bloom": "evaluate",
|
|
2637
|
+
"prompt": "Checkout takes a few minutes: the fan picks a seat, then types card details. How do you stop anyone else taking the seat meanwhile, without locking it forever?",
|
|
2638
|
+
"choices": [
|
|
2639
|
+
"Hold the seat with an expiry, say 10 minutes, and free it if payment doesn't finish",
|
|
2640
|
+
"Keep a database row lock open while the fan types",
|
|
2641
|
+
"Reserve nothing: check again at payment and apologise if it's gone"
|
|
2642
|
+
],
|
|
2643
|
+
"correct": 0,
|
|
2644
|
+
"reasons": [
|
|
2645
|
+
"Nobody else can take it while the clock runs, and an abandoned hold frees itself",
|
|
2646
|
+
"A row lock costs nothing over a few minutes",
|
|
2647
|
+
"Fans rarely abandon checkout"
|
|
2648
|
+
],
|
|
2649
|
+
"reason": 0,
|
|
2650
|
+
"costs": [
|
|
2651
|
+
"Expired holds must count as free again, and held seats look taken",
|
|
2652
|
+
"Database connections tied up for minutes at a time",
|
|
2653
|
+
"Fans lose their seat at the very last step"
|
|
2654
|
+
],
|
|
2655
|
+
"cost": 0,
|
|
2656
|
+
"explain": "Database locks suit milliseconds, not minutes of someone typing. A hold is a conditional write that sets a held-until time; once it passes, the seat counts as free again. For a huge rush on a few seats, add a virtual waiting room that lets people in at a pace you can serve. Say: \"A ten-minute hold with an expiry, claimed atomically.\"",
|
|
2657
|
+
"source": "https://www.hellointerview.com/learn/system-design/problem-breakdowns/ticketmaster"
|
|
2658
|
+
},
|
|
2659
|
+
{
|
|
2660
|
+
"id": "pat-11",
|
|
2661
|
+
"type": "recall",
|
|
2662
|
+
"bloom": "understand",
|
|
2663
|
+
"prompt": "Pessimistic or optimistic locking: when does each fit?",
|
|
2664
|
+
"answer": "Pessimistic (lock the row, then change it) when conflicts are frequent and the lock is brief. Optimistic (check a version number as you write, retry if it moved) when conflicts are rare.",
|
|
2665
|
+
"explain": "Locking first makes everyone else wait, which is fine for a busy row held for milliseconds. A version check on write costs nothing until two writers collide, so it suits rare clashes. Neither suits a hold that lasts minutes: that needs a timed reservation. Say: \"Clashes are rare, so optimistic: a version check on write.\"",
|
|
2666
|
+
"source": "https://en.wikipedia.org/wiki/Optimistic_concurrency_control"
|
|
2667
|
+
},
|
|
2668
|
+
{
|
|
2669
|
+
"id": "pat-12",
|
|
2670
|
+
"type": "fill",
|
|
2671
|
+
"bloom": "apply",
|
|
2672
|
+
"prompt": "Find nearby drivers. Each driver's phone sends its location every 5 seconds; riders ask for the closest drivers. Fill the two empty boxes.",
|
|
2673
|
+
"diagram": {
|
|
2674
|
+
"width": 640,
|
|
2675
|
+
"height": 300,
|
|
2676
|
+
"nodes": [
|
|
2677
|
+
{
|
|
2678
|
+
"id": "drivers",
|
|
2679
|
+
"label": "Driver phones",
|
|
2680
|
+
"x": 20,
|
|
2681
|
+
"y": 40
|
|
2682
|
+
},
|
|
2683
|
+
{
|
|
2684
|
+
"id": "ingest",
|
|
2685
|
+
"label": "?",
|
|
2686
|
+
"x": 240,
|
|
2687
|
+
"y": 40
|
|
2688
|
+
},
|
|
2689
|
+
{
|
|
2690
|
+
"id": "index",
|
|
2691
|
+
"label": "?",
|
|
2692
|
+
"x": 460,
|
|
2693
|
+
"y": 126
|
|
2694
|
+
},
|
|
2695
|
+
{
|
|
2696
|
+
"id": "riders",
|
|
2697
|
+
"label": "Rider app",
|
|
2698
|
+
"x": 20,
|
|
2699
|
+
"y": 212
|
|
2700
|
+
},
|
|
2701
|
+
{
|
|
2702
|
+
"id": "match",
|
|
2703
|
+
"label": "Matching service",
|
|
2704
|
+
"x": 240,
|
|
2705
|
+
"y": 212
|
|
2706
|
+
},
|
|
2707
|
+
{
|
|
2708
|
+
"id": "db",
|
|
2709
|
+
"label": "Database: trips",
|
|
2710
|
+
"x": 480,
|
|
2711
|
+
"y": 240
|
|
2712
|
+
}
|
|
2713
|
+
],
|
|
2714
|
+
"edges": [
|
|
2715
|
+
[
|
|
2716
|
+
"drivers",
|
|
2717
|
+
"ingest",
|
|
2718
|
+
"every 5 s"
|
|
2719
|
+
],
|
|
2720
|
+
[
|
|
2721
|
+
"ingest",
|
|
2722
|
+
"index",
|
|
2723
|
+
"latest point"
|
|
2724
|
+
],
|
|
2725
|
+
[
|
|
2726
|
+
"riders",
|
|
2727
|
+
"match"
|
|
2728
|
+
],
|
|
2729
|
+
[
|
|
2730
|
+
"match",
|
|
2731
|
+
"index",
|
|
2732
|
+
"within 2 km?"
|
|
2733
|
+
],
|
|
2734
|
+
[
|
|
2735
|
+
"match",
|
|
2736
|
+
"db"
|
|
2737
|
+
]
|
|
2738
|
+
]
|
|
2739
|
+
},
|
|
2740
|
+
"slots": {
|
|
2741
|
+
"ingest": "Location service",
|
|
2742
|
+
"index": "In-memory geo index"
|
|
2743
|
+
},
|
|
2744
|
+
"palette": [
|
|
2745
|
+
"Location service",
|
|
2746
|
+
"In-memory geo index",
|
|
2747
|
+
"Nightly batch job",
|
|
2748
|
+
"Full scan of a lat/long table",
|
|
2749
|
+
"CDN"
|
|
2750
|
+
],
|
|
2751
|
+
"explain": "Locations change every few seconds, so keep only each driver's latest point, in memory, in a structure that answers \"who is near here?\" without checking everyone: a geohash- or grid-based index such as Redis GEO. The database keeps trips, not every ping. Say: \"Pings update an in-memory geo index; matching asks it by radius.\"",
|
|
2752
|
+
"source": "https://redis.io/docs/latest/develop/data-types/geospatial/"
|
|
2753
|
+
},
|
|
2754
|
+
{
|
|
2755
|
+
"id": "pat-13",
|
|
2756
|
+
"type": "multi",
|
|
2757
|
+
"bloom": "analyse",
|
|
2758
|
+
"prompt": "A food app shows restaurants within 3 km: 2 million places that rarely move, searched all day. Which of these make the search fast?",
|
|
2759
|
+
"options": [
|
|
2760
|
+
"Index locations by geohash or quadtree cells",
|
|
2761
|
+
"Look up the rider's cell and its neighbours, then filter by exact distance",
|
|
2762
|
+
"Cache results per cell for busy areas",
|
|
2763
|
+
"Use a spatial index, such as PostGIS or Redis GEO",
|
|
2764
|
+
"Sort every restaurant by distance on each request",
|
|
2765
|
+
"Add read replicas and scan the whole table on each one"
|
|
2766
|
+
],
|
|
2767
|
+
"correct": [
|
|
2768
|
+
0,
|
|
2769
|
+
1,
|
|
2770
|
+
2,
|
|
2771
|
+
3
|
|
2772
|
+
],
|
|
2773
|
+
"explain": "A geohash turns a point into a string where a shared prefix means a nearby cell; a quadtree splits busy areas into smaller cells. Search the rider's cell and its neighbours, since a place just over the edge is still near, then filter by real distance. Places rarely move, so busy cells cache well. Say: \"Spatial index, neighbouring cells, then exact distance.\"",
|
|
2774
|
+
"source": "https://en.wikipedia.org/wiki/Geohash"
|
|
2775
|
+
},
|
|
2776
|
+
{
|
|
2777
|
+
"id": "pat-boss",
|
|
2778
|
+
"type": "lightning",
|
|
2779
|
+
"bloom": "understand",
|
|
2780
|
+
"seconds": 120,
|
|
2781
|
+
"prompt": "You hear the problem; name the pattern. Two minutes.",
|
|
2782
|
+
"pairs": [
|
|
2783
|
+
[
|
|
2784
|
+
"Reads dwarf writes and the data changes slowly",
|
|
2785
|
+
"Scale reads: CDN, cache, replicas"
|
|
2786
|
+
],
|
|
2787
|
+
[
|
|
2788
|
+
"A slow multi-table query read all day",
|
|
2789
|
+
"A precomputed read model (CQRS)"
|
|
2790
|
+
],
|
|
2791
|
+
[
|
|
2792
|
+
"Hundreds of thousands of small writes a second",
|
|
2793
|
+
"Scale writes: batch, aggregate, shard"
|
|
2794
|
+
],
|
|
2795
|
+
[
|
|
2796
|
+
"Users upload multi-gigabyte files",
|
|
2797
|
+
"Multipart, presigned uploads to storage"
|
|
2798
|
+
],
|
|
2799
|
+
[
|
|
2800
|
+
"Serving big files to people everywhere",
|
|
2801
|
+
"A CDN in front of object storage"
|
|
2802
|
+
],
|
|
2803
|
+
[
|
|
2804
|
+
"A job takes minutes to finish",
|
|
2805
|
+
"Job ID, queue, status endpoint"
|
|
2806
|
+
],
|
|
2807
|
+
[
|
|
2808
|
+
"Steps across services that must survive a crash",
|
|
2809
|
+
"A workflow engine"
|
|
2810
|
+
],
|
|
2811
|
+
[
|
|
2812
|
+
"Browsers must see changes as they happen",
|
|
2813
|
+
"SSE or WebSockets, plus pub/sub"
|
|
2814
|
+
],
|
|
2815
|
+
[
|
|
2816
|
+
"Two people try to book the last seat at once",
|
|
2817
|
+
"An atomic claim: conditional write or version check"
|
|
2818
|
+
],
|
|
2819
|
+
[
|
|
2820
|
+
"The seat must stay yours while you pay",
|
|
2821
|
+
"A reservation hold with an expiry"
|
|
2822
|
+
],
|
|
2823
|
+
[
|
|
2824
|
+
"Find the drivers within 2 km of a rider",
|
|
2825
|
+
"A geo index: geohash, quadtree, Redis GEO"
|
|
2826
|
+
]
|
|
2827
|
+
],
|
|
2828
|
+
"explain": "Most designs are a few of these patterns joined up. Naming the pattern first gives your answer a shape; then say its cost. If one was slow, say that pairing aloud before the next round.",
|
|
2829
|
+
"source": "https://www.hellointerview.com/learn/system-design/in-a-hurry/patterns"
|
|
2830
|
+
}
|
|
2831
|
+
]
|
|
2832
|
+
},
|
|
2833
|
+
{
|
|
2834
|
+
"chapter": 9,
|
|
2835
|
+
"slug": "url-shortener",
|
|
2836
|
+
"kind": "quest",
|
|
2837
|
+
"title": "URL shortener (like Bitly)",
|
|
2838
|
+
"emoji": "🔗",
|
|
2839
|
+
"pillar": "Quests",
|
|
2840
|
+
"builds_on": [
|
|
2841
|
+
1,
|
|
2842
|
+
2,
|
|
2843
|
+
4,
|
|
2844
|
+
5
|
|
2845
|
+
],
|
|
2846
|
+
"blurb": "Turn long links into short ones that redirect fast. Requirements, numbers, API, the design, then the deep dives.",
|
|
2847
|
+
"boss": "fb-boss",
|
|
2848
|
+
"challenges": [
|
|
2849
|
+
{
|
|
2850
|
+
"id": "fb-01",
|
|
2851
|
+
"type": "sprint",
|
|
2852
|
+
"bloom": "create",
|
|
2853
|
+
"minutes": 10,
|
|
2854
|
+
"prompt": "Requirements for a URL shortener (people turn long links into short ones and share them). Out loud, against the clock: read it back, who uses it and what they do, your three hard parts, how busy (with the peak), what can't break, what's out.",
|
|
2855
|
+
"checks": [
|
|
2856
|
+
"Read the problem back in one line",
|
|
2857
|
+
"Named both users: people who create links and people who click them",
|
|
2858
|
+
"Picked my own three hard parts, such as unique codes, fast redirects at the peak, and expiry or click counts",
|
|
2859
|
+
"Asked for the scale, the peak and the read-to-write ratio",
|
|
2860
|
+
"Said what can't break, such as a code never pointing to the wrong place",
|
|
2861
|
+
"Said \"I'll assume…\" for scope, instead of asking \"do you want…?\"",
|
|
2862
|
+
"Said what's out of scope",
|
|
2863
|
+
"Finished inside the time"
|
|
2864
|
+
],
|
|
2865
|
+
"explain": "This problem rewards the same first quarter as any other: a clear read-back, both kinds of user, numbers with a peak, and your own picks. Redirects vastly outnumber new links, which shapes the whole design. If a check missed, rehearse that line before the next round.",
|
|
2866
|
+
"source": "https://www.hellointerview.com/learn/system-design/problem-breakdowns/bitly"
|
|
2867
|
+
},
|
|
2868
|
+
{
|
|
2869
|
+
"id": "fb-02",
|
|
2870
|
+
"type": "estimate",
|
|
2871
|
+
"bloom": "apply",
|
|
2872
|
+
"prompt": "Redirects for a popular link shortener. Estimate the busiest second.",
|
|
2873
|
+
"generator": {
|
|
2874
|
+
"kind": "peak_rps",
|
|
2875
|
+
"params": {
|
|
2876
|
+
"users": [
|
|
2877
|
+
1000000,
|
|
2878
|
+
200000000
|
|
2879
|
+
],
|
|
2880
|
+
"actions": [
|
|
2881
|
+
1,
|
|
2882
|
+
10
|
|
2883
|
+
]
|
|
2884
|
+
}
|
|
2885
|
+
},
|
|
2886
|
+
"explain": "Clicks a day, divided by about 100,000 seconds, times 10 for the peak. Redirects are reads, so this number sizes the cache and the app servers, while new links stay a trickle. Say the working aloud and round hard.",
|
|
2887
|
+
"source": "https://www.hellointerview.com/learn/system-design/in-a-hurry/core-concepts"
|
|
2888
|
+
},
|
|
2889
|
+
{
|
|
2890
|
+
"id": "fb-03",
|
|
2891
|
+
"type": "write",
|
|
2892
|
+
"bloom": "apply",
|
|
2893
|
+
"prompt": "Name the core entities, then sketch the API: create a short link, and follow one.",
|
|
2894
|
+
"key_points": [
|
|
2895
|
+
"Entities: a short link (code, long URL, owner, created, optional expiry) and a user",
|
|
2896
|
+
"POST /urls with the long URL, an optional custom alias and expiry; returns 201 and the code",
|
|
2897
|
+
"GET /{code} answers with a redirect to the long URL",
|
|
2898
|
+
"404 for an unknown code, 410 Gone for an expired one",
|
|
2899
|
+
"The owner comes from the token, never from the request body"
|
|
2900
|
+
],
|
|
2901
|
+
"model": "Entities: ShortLink (code, long URL, owner, created at, expires at) and User. POST /urls takes the long URL, an optional alias and expiry, and returns 201 with the short code; the owner comes from the token. GET /{code} returns a redirect to the long URL, 404 if the code is unknown and 410 if it has expired.",
|
|
2902
|
+
"explain": "Two endpoints carry the whole product, so name them early and keep them plain. The redirect is the hot path; creating links is rare. Say: \"Entities first, then the API is just verbs on them.\"",
|
|
2903
|
+
"source": "https://www.hellointerview.com/learn/system-design/in-a-hurry/delivery"
|
|
2904
|
+
},
|
|
2905
|
+
{
|
|
2906
|
+
"id": "fb-04",
|
|
2907
|
+
"type": "fill",
|
|
2908
|
+
"bloom": "apply",
|
|
2909
|
+
"prompt": "Land the design end to end. Drag each part into its box: the hot lookup, the source of truth, where new codes come from, and how clicks get counted without slowing the redirect.",
|
|
2910
|
+
"diagram": {
|
|
2911
|
+
"width": 640,
|
|
2912
|
+
"height": 300,
|
|
2913
|
+
"nodes": [
|
|
2914
|
+
{
|
|
2915
|
+
"id": "browser",
|
|
2916
|
+
"label": "Browser",
|
|
2917
|
+
"x": 10,
|
|
2918
|
+
"y": 126
|
|
2919
|
+
},
|
|
2920
|
+
{
|
|
2921
|
+
"id": "lb",
|
|
2922
|
+
"label": "Load balancer",
|
|
2923
|
+
"x": 140,
|
|
2924
|
+
"y": 126
|
|
2925
|
+
},
|
|
2926
|
+
{
|
|
2927
|
+
"id": "app",
|
|
2928
|
+
"label": "App servers",
|
|
2929
|
+
"x": 270,
|
|
2930
|
+
"y": 126
|
|
2931
|
+
},
|
|
2932
|
+
{
|
|
2933
|
+
"id": "s-counter",
|
|
2934
|
+
"label": "?",
|
|
2935
|
+
"x": 270,
|
|
2936
|
+
"y": 20
|
|
2937
|
+
},
|
|
2938
|
+
{
|
|
2939
|
+
"id": "s-cache",
|
|
2940
|
+
"label": "?",
|
|
2941
|
+
"x": 396,
|
|
2942
|
+
"y": 20
|
|
2943
|
+
},
|
|
2944
|
+
{
|
|
2945
|
+
"id": "s-db",
|
|
2946
|
+
"label": "?",
|
|
2947
|
+
"x": 396,
|
|
2948
|
+
"y": 126
|
|
2949
|
+
},
|
|
2950
|
+
{
|
|
2951
|
+
"id": "s-queue",
|
|
2952
|
+
"label": "?",
|
|
2953
|
+
"x": 396,
|
|
2954
|
+
"y": 232
|
|
2955
|
+
},
|
|
2956
|
+
{
|
|
2957
|
+
"id": "workers",
|
|
2958
|
+
"label": "Click-count workers",
|
|
2959
|
+
"x": 522,
|
|
2960
|
+
"y": 232,
|
|
2961
|
+
"w": 114
|
|
2962
|
+
}
|
|
2963
|
+
],
|
|
2964
|
+
"edges": [
|
|
2965
|
+
[
|
|
2966
|
+
"browser",
|
|
2967
|
+
"lb"
|
|
2968
|
+
],
|
|
2969
|
+
[
|
|
2970
|
+
"lb",
|
|
2971
|
+
"app"
|
|
2972
|
+
],
|
|
2973
|
+
[
|
|
2974
|
+
"app",
|
|
2975
|
+
"s-counter",
|
|
2976
|
+
"block of IDs"
|
|
2977
|
+
],
|
|
2978
|
+
[
|
|
2979
|
+
"app",
|
|
2980
|
+
"s-cache",
|
|
2981
|
+
"look up code"
|
|
2982
|
+
],
|
|
2983
|
+
[
|
|
2984
|
+
"app",
|
|
2985
|
+
"s-db",
|
|
2986
|
+
"on a miss"
|
|
2987
|
+
],
|
|
2988
|
+
[
|
|
2989
|
+
"app",
|
|
2990
|
+
"s-queue",
|
|
2991
|
+
"click event"
|
|
2992
|
+
],
|
|
2993
|
+
[
|
|
2994
|
+
"s-queue",
|
|
2995
|
+
"workers"
|
|
2996
|
+
]
|
|
2997
|
+
]
|
|
2998
|
+
},
|
|
2999
|
+
"slots": {
|
|
3000
|
+
"s-cache": "Redis cache",
|
|
3001
|
+
"s-db": "Database: code → long URL",
|
|
3002
|
+
"s-counter": "ID counter",
|
|
3003
|
+
"s-queue": "Queue"
|
|
3004
|
+
},
|
|
3005
|
+
"palette": [
|
|
3006
|
+
"Redis cache",
|
|
3007
|
+
"Database: code → long URL",
|
|
3008
|
+
"ID counter",
|
|
3009
|
+
"Queue",
|
|
3010
|
+
"WebSocket server",
|
|
3011
|
+
"Graph database",
|
|
3012
|
+
"Search index"
|
|
3013
|
+
],
|
|
3014
|
+
"explain": "The cache answers almost every redirect; the database is the truth behind it; a counter hands out unique numbers for new codes; clicks go onto a queue so counting never slows a redirect. Say each box's reason as you place it, then: \"That's the whole path. The deep dives are codes and redirect speed.\"",
|
|
3015
|
+
"source": "https://www.hellointerview.com/learn/system-design/problem-breakdowns/bitly"
|
|
3016
|
+
},
|
|
3017
|
+
{
|
|
3018
|
+
"id": "fb-05",
|
|
3019
|
+
"type": "duel",
|
|
3020
|
+
"bloom": "evaluate",
|
|
3021
|
+
"prompt": "Marketing wants every click on a short link counted. Which redirect do you send?",
|
|
3022
|
+
"choices": [
|
|
3023
|
+
"302 Found, a temporary redirect",
|
|
3024
|
+
"301 Moved Permanently"
|
|
3025
|
+
],
|
|
3026
|
+
"correct": 0,
|
|
3027
|
+
"reasons": [
|
|
3028
|
+
"Browsers don't remember a 302, so every click comes back to you and is counted",
|
|
3029
|
+
"A 302 is faster for the person clicking",
|
|
3030
|
+
"A 301 can't carry a long URL"
|
|
3031
|
+
],
|
|
3032
|
+
"reason": 0,
|
|
3033
|
+
"costs": [
|
|
3034
|
+
"Every click is a request you serve, so the cache and servers carry more load",
|
|
3035
|
+
"Links stop working after a day",
|
|
3036
|
+
"None"
|
|
3037
|
+
],
|
|
3038
|
+
"cost": 0,
|
|
3039
|
+
"explain": "A 301 tells browsers to remember the target, so repeat clicks skip your servers: less load, but you lose the count. A 302 brings every click back to you. Say: \"302, because counting clicks is a requirement; the cache absorbs the load.\"",
|
|
3040
|
+
"source": "https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/302"
|
|
3041
|
+
},
|
|
3042
|
+
{
|
|
3043
|
+
"id": "fb-06",
|
|
3044
|
+
"type": "multi",
|
|
3045
|
+
"bloom": "analyse",
|
|
3046
|
+
"prompt": "Redirects must answer in under 50 ms at the peak. Which of these help?",
|
|
3047
|
+
"options": [
|
|
3048
|
+
"Cache code → URL in Redis, evicting the least used",
|
|
3049
|
+
"Keep app servers stateless and add more at the peak",
|
|
3050
|
+
"Read replicas for cache misses",
|
|
3051
|
+
"Put link creation on a queue",
|
|
3052
|
+
"Shard the links table by creation date",
|
|
3053
|
+
"Index the long URL column"
|
|
3054
|
+
],
|
|
3055
|
+
"correct": [
|
|
3056
|
+
0,
|
|
3057
|
+
1,
|
|
3058
|
+
2
|
|
3059
|
+
],
|
|
3060
|
+
"explain": "A small share of links gets most clicks, so a cache holding the popular codes answers nearly everything in about a millisecond. Stateless servers scale out, and replicas take the misses. Queuing creation doesn't touch the redirect path, sharding by date piles new writes on one shard, and the long-URL index only helps spot duplicates. Say: \"Cache the hot codes; replicas behind it.\"",
|
|
3061
|
+
"source": "https://www.hellointerview.com/learn/system-design/core-concepts/caching"
|
|
3062
|
+
},
|
|
3063
|
+
{
|
|
3064
|
+
"id": "fb-boss",
|
|
3065
|
+
"type": "duel",
|
|
3066
|
+
"bloom": "evaluate",
|
|
3067
|
+
"prompt": "Deep dive: how do you make the short codes? Seven characters, billions of links, and several app servers creating them at once.",
|
|
3068
|
+
"choices": [
|
|
3069
|
+
"A counter, written in base62 and handed out in blocks",
|
|
3070
|
+
"Hash the long URL and keep the first seven characters",
|
|
3071
|
+
"Seven random characters, checked against the database"
|
|
3072
|
+
],
|
|
3073
|
+
"correct": 0,
|
|
3074
|
+
"reasons": [
|
|
3075
|
+
"Each number is used once, so codes never clash and nothing needs checking first",
|
|
3076
|
+
"Hashing guarantees no two links ever share a code",
|
|
3077
|
+
"Random codes can never clash"
|
|
3078
|
+
],
|
|
3079
|
+
"reason": 0,
|
|
3080
|
+
"costs": [
|
|
3081
|
+
"Sequential codes are guessable unless scrambled, and the counter must stay up",
|
|
3082
|
+
"Every link needs a lookup in a global index",
|
|
3083
|
+
"Codes get longer every year"
|
|
3084
|
+
],
|
|
3085
|
+
"cost": 0,
|
|
3086
|
+
"explain": "62 characters in seven places give about 3.5 trillion codes. A counter guarantees uniqueness, and giving each app server a block of 1,000 numbers at a time saves a round trip per link and survives a short counter outage. Truncated hashes collide; random codes need a check that slows as the space fills. Say: \"Counter plus base62, handed out in blocks.\"",
|
|
3087
|
+
"source": "https://www.hellointerview.com/learn/system-design/problem-breakdowns/bitly"
|
|
3088
|
+
}
|
|
3089
|
+
]
|
|
3090
|
+
},
|
|
3091
|
+
{
|
|
3092
|
+
"chapter": 10,
|
|
3093
|
+
"slug": "rate-limiter",
|
|
3094
|
+
"kind": "quest",
|
|
3095
|
+
"title": "Rate limiter",
|
|
3096
|
+
"emoji": "🚦",
|
|
3097
|
+
"pillar": "Quests",
|
|
3098
|
+
"builds_on": [
|
|
3099
|
+
1,
|
|
3100
|
+
2,
|
|
3101
|
+
5,
|
|
3102
|
+
6
|
|
3103
|
+
],
|
|
3104
|
+
"blurb": "Cap how often each client can call an API (application programming interface). Requirements, numbers, the interface, the design, deep dives.",
|
|
3105
|
+
"boss": "rl-boss",
|
|
3106
|
+
"challenges": [
|
|
3107
|
+
{
|
|
3108
|
+
"id": "rl-01",
|
|
3109
|
+
"type": "sprint",
|
|
3110
|
+
"bloom": "create",
|
|
3111
|
+
"minutes": 10,
|
|
3112
|
+
"prompt": "Requirements for a rate limiter (a shared check that caps how many requests each client can make to an API in a period of time). Out loud, against the clock: read it back, who uses it and what they do, your three hard parts, how busy (with the peak), what can't break, what's out.",
|
|
3113
|
+
"checks": [
|
|
3114
|
+
"Read the problem back in one line",
|
|
3115
|
+
"Named both users: clients calling the API, and the teams who set the limits",
|
|
3116
|
+
"Picked my own three hard parts, such as counting across many servers, adding almost no delay, and surviving a counter outage",
|
|
3117
|
+
"Asked for the scale and the peak: requests a second, how many clients, how many rules",
|
|
3118
|
+
"Said what can't break, such as the limiter never taking the API down with it",
|
|
3119
|
+
"Said \"I'll assume…\" for scope, instead of asking \"do you want…?\"",
|
|
3120
|
+
"Said what's out of scope, such as billing quotas or blocking network-level floods",
|
|
3121
|
+
"Finished inside the time"
|
|
3122
|
+
],
|
|
3123
|
+
"explain": "A rate limiter sits on every request's path, so the first quarter should settle who is limited, by which key, how strict the count must be and how busy the API gets. The hard parts follow: shared counts, speed and staying up. If a check missed, rehearse that line before the next round. Say: \"I'll assume limits per user or API key, checked on every request, with counts shared across servers.\"",
|
|
3124
|
+
"source": "https://www.hellointerview.com/learn/system-design/problem-breakdowns/distributed-rate-limiter"
|
|
3125
|
+
},
|
|
3126
|
+
{
|
|
3127
|
+
"id": "rl-02",
|
|
3128
|
+
"type": "estimate",
|
|
3129
|
+
"bloom": "apply",
|
|
3130
|
+
"prompt": "A public API, and every call passes through the rate limiter first. Estimate the busiest second the limiter must check.",
|
|
3131
|
+
"generator": {
|
|
3132
|
+
"kind": "peak_rps",
|
|
3133
|
+
"params": {
|
|
3134
|
+
"users": [
|
|
3135
|
+
1000000,
|
|
3136
|
+
100000000
|
|
3137
|
+
],
|
|
3138
|
+
"actions": [
|
|
3139
|
+
10,
|
|
3140
|
+
100
|
|
3141
|
+
]
|
|
3142
|
+
}
|
|
3143
|
+
},
|
|
3144
|
+
"explain": "Calls a day, divided by about 100,000 seconds, times 10 for the peak. Every call costs at least one counter update, so this number sizes the counter store: one Redis node handles very roughly 100,000 simple operations a second, so a big answer means splitting keys across several nodes. Say: \"That many checks a second at the peak, so I'll spread the counters across nodes by key.\"",
|
|
3145
|
+
"source": "https://www.hellointerview.com/learn/system-design/problem-breakdowns/distributed-rate-limiter"
|
|
3146
|
+
},
|
|
3147
|
+
{
|
|
3148
|
+
"id": "rl-03",
|
|
3149
|
+
"type": "write",
|
|
3150
|
+
"bloom": "apply",
|
|
3151
|
+
"prompt": "Name the core entities, then sketch the limiter's interface: what a server asks it on each request, and what the client sees when it's over the limit.",
|
|
3152
|
+
"key_points": [
|
|
3153
|
+
"Entities: a rule (whom it covers, the limit, the window or refill rate) and each key's current count or bucket",
|
|
3154
|
+
"One check per request: given a key and a rule, return allowed or not, what's left and when it resets",
|
|
3155
|
+
"The key names the client: a user ID, an API key, an IP address or a tenant",
|
|
3156
|
+
"Over the limit: status 429 Too Many Requests, with a Retry-After header saying how many seconds to wait",
|
|
3157
|
+
"Headers on every response give the limit, what's left and the reset time, so clients can slow down early",
|
|
3158
|
+
"Rules sit in config that each server caches in memory, never read from a database per request"
|
|
3159
|
+
],
|
|
3160
|
+
"model": "Entities: Rule (key type, limit, window or refill rate) and Counter (a key's current count or token bucket). Internally, check(key, rule) returns allowed, remaining and reset time. Allowed responses carry headers with the limit, what's left and the reset time, such as X-RateLimit-Remaining. Rejected ones get 429 Too Many Requests with Retry-After: 30 and the same headers; the service behind never sees them.",
|
|
3161
|
+
"explain": "The limiter answers one question per request, so its interface is tiny: is this key allowed, and how much is left? The client's side matters more: a clear 429 and a Retry-After stop well-behaved clients retrying in a tight loop and making the overload worse. Say: \"One check per request; over the limit, 429 with Retry-After and the remaining-quota headers.\"",
|
|
3162
|
+
"source": "https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/429"
|
|
3163
|
+
},
|
|
3164
|
+
{
|
|
3165
|
+
"id": "rl-04",
|
|
3166
|
+
"type": "fill",
|
|
3167
|
+
"bloom": "apply",
|
|
3168
|
+
"prompt": "Land the design end to end. Drag each part into its box: where the check happens before any real work, where counts are shared across servers, and where the limits are defined.",
|
|
3169
|
+
"diagram": {
|
|
3170
|
+
"width": 640,
|
|
3171
|
+
"height": 300,
|
|
3172
|
+
"nodes": [
|
|
3173
|
+
{
|
|
3174
|
+
"id": "clients",
|
|
3175
|
+
"label": "Clients",
|
|
3176
|
+
"x": 10,
|
|
3177
|
+
"y": 126
|
|
3178
|
+
},
|
|
3179
|
+
{
|
|
3180
|
+
"id": "s-gateway",
|
|
3181
|
+
"label": "?",
|
|
3182
|
+
"x": 230,
|
|
3183
|
+
"y": 126
|
|
3184
|
+
},
|
|
3185
|
+
{
|
|
3186
|
+
"id": "s-counters",
|
|
3187
|
+
"label": "?",
|
|
3188
|
+
"x": 450,
|
|
3189
|
+
"y": 10
|
|
3190
|
+
},
|
|
3191
|
+
{
|
|
3192
|
+
"id": "s-rules",
|
|
3193
|
+
"label": "?",
|
|
3194
|
+
"x": 450,
|
|
3195
|
+
"y": 242
|
|
3196
|
+
},
|
|
3197
|
+
{
|
|
3198
|
+
"id": "services",
|
|
3199
|
+
"label": "API services",
|
|
3200
|
+
"x": 510,
|
|
3201
|
+
"y": 126
|
|
3202
|
+
}
|
|
3203
|
+
],
|
|
3204
|
+
"edges": [
|
|
3205
|
+
[
|
|
3206
|
+
"clients",
|
|
3207
|
+
"s-gateway",
|
|
3208
|
+
"every request"
|
|
3209
|
+
],
|
|
3210
|
+
[
|
|
3211
|
+
"s-gateway",
|
|
3212
|
+
"services",
|
|
3213
|
+
"allowed"
|
|
3214
|
+
],
|
|
3215
|
+
[
|
|
3216
|
+
"s-gateway",
|
|
3217
|
+
"s-counters",
|
|
3218
|
+
"count per key"
|
|
3219
|
+
],
|
|
3220
|
+
[
|
|
3221
|
+
"s-rules",
|
|
3222
|
+
"s-gateway",
|
|
3223
|
+
"limits"
|
|
3224
|
+
]
|
|
3225
|
+
]
|
|
3226
|
+
},
|
|
3227
|
+
"slots": {
|
|
3228
|
+
"s-gateway": "API gateway with the limiter",
|
|
3229
|
+
"s-counters": "Redis: a counter per key",
|
|
3230
|
+
"s-rules": "Rules config, cached locally"
|
|
3231
|
+
},
|
|
3232
|
+
"palette": [
|
|
3233
|
+
"API gateway with the limiter",
|
|
3234
|
+
"Redis: a counter per key",
|
|
3235
|
+
"Rules config, cached locally",
|
|
3236
|
+
"Database table: a row per request",
|
|
3237
|
+
"Message queue",
|
|
3238
|
+
"Search index"
|
|
3239
|
+
],
|
|
3240
|
+
"explain": "The check runs at the front door, in the API gateway, so a rejected request costs a millisecond and never reaches the expensive services. Counts live in Redis because many gateway servers see the same client; each server keeps the rules in memory and refreshes them now and then. Say: \"The gateway checks a shared Redis counter per key, with rules cached locally, and rejects before any real work.\"",
|
|
3241
|
+
"source": "https://www.hellointerview.com/learn/system-design/problem-breakdowns/distributed-rate-limiter"
|
|
3242
|
+
},
|
|
3243
|
+
{
|
|
3244
|
+
"id": "rl-05",
|
|
3245
|
+
"type": "duel",
|
|
3246
|
+
"bloom": "evaluate",
|
|
3247
|
+
"prompt": "Deep dive: ten gateway servers share a Redis counter per API key. Each one reads the count, compares it with the limit, then writes count + 1. Under load, clients get past their limit. What do you change?",
|
|
3248
|
+
"choices": [
|
|
3249
|
+
"Check and count in one atomic step: INCR, or a Lua script",
|
|
3250
|
+
"Keep a separate counter in each gateway's memory",
|
|
3251
|
+
"Wrap each check in a distributed lock on the key"
|
|
3252
|
+
],
|
|
3253
|
+
"correct": 0,
|
|
3254
|
+
"reasons": [
|
|
3255
|
+
"Redis runs one command or script at a time, so no other server can slip in between",
|
|
3256
|
+
"A Lua script is faster than any single command",
|
|
3257
|
+
"Atomic steps stop the key from ever expiring"
|
|
3258
|
+
],
|
|
3259
|
+
"reason": 0,
|
|
3260
|
+
"costs": [
|
|
3261
|
+
"Logic moves into Redis scripts, and a slow script holds up every other check",
|
|
3262
|
+
"Each check needs two network round trips instead of one",
|
|
3263
|
+
"Counts reset whenever a gateway restarts"
|
|
3264
|
+
],
|
|
3265
|
+
"cost": 0,
|
|
3266
|
+
"explain": "Read, compare, write is three steps, so two servers can both read 99 and both let a request through. A single INCR (set its EXPIRE in the same transaction) or a Lua script makes Redis take the whole decision as one step. A lock adds round trips, and local counters each see only a tenth of the traffic. Say: \"Check and count atomically in Redis, in one script per request.\"",
|
|
3267
|
+
"source": "https://redis.io/docs/latest/commands/incr/"
|
|
3268
|
+
},
|
|
3269
|
+
{
|
|
3270
|
+
"id": "rl-06",
|
|
3271
|
+
"type": "multi",
|
|
3272
|
+
"bloom": "analyse",
|
|
3273
|
+
"prompt": "Deep dive: every request waits on the counter store, and the API runs in three regions. Which of these keep the limiter fast and available?",
|
|
3274
|
+
"options": [
|
|
3275
|
+
"Spread counters across Redis nodes by a hash of the key",
|
|
3276
|
+
"Give each node a replica that takes over when it fails",
|
|
3277
|
+
"Count within each region, accepting a small overshoot of the global limit",
|
|
3278
|
+
"Decide per API whether to let requests through or block them when Redis is down",
|
|
3279
|
+
"Send every check to one Redis in a single home region",
|
|
3280
|
+
"Retry each check until Redis answers, however long that takes",
|
|
3281
|
+
"Record every request as a row in the main database"
|
|
3282
|
+
],
|
|
3283
|
+
"correct": [
|
|
3284
|
+
0,
|
|
3285
|
+
1,
|
|
3286
|
+
2,
|
|
3287
|
+
3
|
|
3288
|
+
],
|
|
3289
|
+
"explain": "Hashing keys spreads the load, and a replica covers a failure. A cross-ocean check adds tens of milliseconds, so count per region. Failing open (letting requests through) keeps the API up; failing closed guards a fragile backend or a login limit. Endless retries or a row per request turn the limiter into the outage. Say: \"Shard by key, replicate, count per region, fail open or closed per API.\"",
|
|
3290
|
+
"source": "https://www.hellointerview.com/learn/system-design/problem-breakdowns/distributed-rate-limiter"
|
|
3291
|
+
},
|
|
3292
|
+
{
|
|
3293
|
+
"id": "rl-boss",
|
|
3294
|
+
"type": "duel",
|
|
3295
|
+
"bloom": "evaluate",
|
|
3296
|
+
"prompt": "Deep dive: pick the algorithm. Each API key gets 100 requests a minute. Short bursts are fine, but nobody should get 200 through in a few seconds, and the counter store holds millions of keys.",
|
|
3297
|
+
"choices": [
|
|
3298
|
+
"Token bucket: holds 100 tokens, refilled at about 1.7 a second",
|
|
3299
|
+
"Fixed window: one counter per key for each clock minute",
|
|
3300
|
+
"Sliding-window log: a timestamp stored for every request"
|
|
3301
|
+
],
|
|
3302
|
+
"correct": 0,
|
|
3303
|
+
"reasons": [
|
|
3304
|
+
"It allows a burst up to the bucket's size, then holds the average to the refill rate",
|
|
3305
|
+
"It is exact to the millisecond, like a log",
|
|
3306
|
+
"It needs no state shared between servers"
|
|
3307
|
+
],
|
|
3308
|
+
"reason": 0,
|
|
3309
|
+
"costs": [
|
|
3310
|
+
"Two settings to tune, and tokens plus a timestamp updated together in one script",
|
|
3311
|
+
"Memory for a timestamp per request",
|
|
3312
|
+
"Up to twice the limit around each window boundary"
|
|
3313
|
+
],
|
|
3314
|
+
"cost": 0,
|
|
3315
|
+
"explain": "A fixed window lets 100 through just before the minute turns and 100 just after: double the limit in two seconds. A log is exact but keeps up to 100 timestamps per key, for millions of keys. A token bucket keeps two numbers, takes a burst up to its size and holds to the refill rate. A sliding-window counter is the other cheap pick. Say: \"Token bucket: bursts up to 100, then about 1.7 a second.\"",
|
|
3316
|
+
"source": "https://www.hellointerview.com/learn/system-design/problem-breakdowns/distributed-rate-limiter"
|
|
3317
|
+
}
|
|
3318
|
+
]
|
|
3319
|
+
},
|
|
3320
|
+
{
|
|
3321
|
+
"chapter": 11,
|
|
3322
|
+
"slug": "file-sync",
|
|
3323
|
+
"kind": "quest",
|
|
3324
|
+
"title": "File storage and sync (like Dropbox)",
|
|
3325
|
+
"emoji": "📁",
|
|
3326
|
+
"pillar": "Quests",
|
|
3327
|
+
"builds_on": [
|
|
3328
|
+
1,
|
|
3329
|
+
3,
|
|
3330
|
+
4,
|
|
3331
|
+
8
|
|
3332
|
+
],
|
|
3333
|
+
"blurb": "Keep every device's files in step. Requirements, numbers, API, the design, then the deep dives: big uploads, sync and clashing edits.",
|
|
3334
|
+
"boss": "fs-boss",
|
|
3335
|
+
"challenges": [
|
|
3336
|
+
{
|
|
3337
|
+
"id": "fs-01",
|
|
3338
|
+
"type": "sprint",
|
|
3339
|
+
"bloom": "create",
|
|
3340
|
+
"minutes": 10,
|
|
3341
|
+
"prompt": "Requirements for a file storage and sync service (people save files in a folder that matches on every device, and share some). Out loud, against the clock: read it back, who uses it and what they do, your three hard parts, how busy (with the peak), what can't break, what's out.",
|
|
3342
|
+
"checks": [
|
|
3343
|
+
"Read the problem back in one line",
|
|
3344
|
+
"Named the users and what they do: save, edit offline, see changes on other devices, share",
|
|
3345
|
+
"Picked my own three hard parts, such as big uploads, fast sync across devices, and offline edits that clash",
|
|
3346
|
+
"Sized it: users, files a day, typical and largest file size, and the peak",
|
|
3347
|
+
"Said what can't break: a saved file is never lost or silently overwritten",
|
|
3348
|
+
"Said \"I'll assume…\" for scope, instead of asking \"do you want…?\"",
|
|
3349
|
+
"Said what's out of scope, such as editing inside the browser or searching file contents",
|
|
3350
|
+
"Finished inside the time"
|
|
3351
|
+
],
|
|
3352
|
+
"explain": "This product turns on three things: big files, many devices and edits made while offline. Say early what can't break: a saved file is never lost or silently overwritten, while a few seconds' delay before another device sees it is fine. If a check missed, rehearse that line before the next round. Say: \"Never lose a file; sync within seconds is good enough.\"",
|
|
3353
|
+
"source": "https://www.hellointerview.com/learn/system-design/problem-breakdowns/dropbox"
|
|
3354
|
+
},
|
|
3355
|
+
{
|
|
3356
|
+
"id": "fs-02",
|
|
3357
|
+
"type": "estimate",
|
|
3358
|
+
"bloom": "apply",
|
|
3359
|
+
"prompt": "At the peak, devices download changed file chunks of a few MB each. Estimate the outbound bandwidth. (KB, MB and GB are kilobytes, megabytes and gigabytes; each is 1,000 times the last.)",
|
|
3360
|
+
"generator": {
|
|
3361
|
+
"kind": "bandwidth",
|
|
3362
|
+
"params": {
|
|
3363
|
+
"rps": [
|
|
3364
|
+
500,
|
|
3365
|
+
20000
|
|
3366
|
+
],
|
|
3367
|
+
"kb": [
|
|
3368
|
+
1000,
|
|
3369
|
+
5000
|
|
3370
|
+
]
|
|
3371
|
+
}
|
|
3372
|
+
},
|
|
3373
|
+
"explain": "Responses a second times the size of each, then divide by 1,000 to turn KB into MB. Thousands of multi-MB chunks a second come to many GB a second, too much for app servers to carry. The bytes come from object storage through a CDN (content delivery network) on signed links, and app servers handle only small metadata calls. Say: \"Bytes from storage and the CDN; the app tier moves metadata.\"",
|
|
3374
|
+
"source": "https://docs.aws.amazon.com/AmazonS3/latest/userguide/using-presigned-url.html"
|
|
3375
|
+
},
|
|
3376
|
+
{
|
|
3377
|
+
"id": "fs-03",
|
|
3378
|
+
"type": "write",
|
|
3379
|
+
"bloom": "apply",
|
|
3380
|
+
"prompt": "Name the core entities, then sketch the API (application programming interface): start an upload, get upload links, commit it, list changes since a cursor, and download a file.",
|
|
3381
|
+
"key_points": [
|
|
3382
|
+
"Entities: file (path, owner), version (number, base version, ordered chunk hashes), chunk (hash, size) and device",
|
|
3383
|
+
"POST /uploads with the path, base version and chunk hashes; returns an upload ID (identifier)",
|
|
3384
|
+
"It also returns presigned URLs (signed, short-lived web addresses), one per chunk storage lacks",
|
|
3385
|
+
"POST /uploads/{id}/commit makes the new version live only once every chunk is stored",
|
|
3386
|
+
"GET /changes?cursor=… returns what changed since then, plus the next cursor to save",
|
|
3387
|
+
"GET /files/{id} returns metadata and short-lived download links; the user comes from the token"
|
|
3388
|
+
],
|
|
3389
|
+
"model": "Entities: File (ID, owner, path, current version), Version (file, number, base version, chunk hashes in order), Chunk (hash, size) and Device. POST /uploads takes the path, base version and chunk hashes; it returns an upload ID and presigned URLs for the chunks storage lacks. POST /uploads/{id}/commit creates the version once every chunk is there. GET /changes?cursor=c returns changes and the next cursor. GET /files/{id} returns metadata and download links.",
|
|
3390
|
+
"explain": "Start, send, then commit keeps big bytes off your servers and stops a half-sent file from ever looking finished. The changes feed with a cursor is what sync runs on, so name it early. Say: \"The API moves metadata; storage moves the bytes.\"",
|
|
3391
|
+
"source": "https://www.hellointerview.com/learn/system-design/in-a-hurry/delivery"
|
|
3392
|
+
},
|
|
3393
|
+
{
|
|
3394
|
+
"id": "fs-04",
|
|
3395
|
+
"type": "fill",
|
|
3396
|
+
"bloom": "apply",
|
|
3397
|
+
"prompt": "Land the design end to end. Drag each part into its box: where the chunks live, the source of truth for files and versions, what serves downloads near the user, and what tells other devices to sync.",
|
|
3398
|
+
"diagram": {
|
|
3399
|
+
"width": 640,
|
|
3400
|
+
"height": 300,
|
|
3401
|
+
"nodes": [
|
|
3402
|
+
{
|
|
3403
|
+
"id": "apps",
|
|
3404
|
+
"label": "Desktop and phone apps",
|
|
3405
|
+
"x": 10,
|
|
3406
|
+
"y": 126
|
|
3407
|
+
},
|
|
3408
|
+
{
|
|
3409
|
+
"id": "s-cdn",
|
|
3410
|
+
"label": "?",
|
|
3411
|
+
"x": 230,
|
|
3412
|
+
"y": 10
|
|
3413
|
+
},
|
|
3414
|
+
{
|
|
3415
|
+
"id": "s-blob",
|
|
3416
|
+
"label": "?",
|
|
3417
|
+
"x": 480,
|
|
3418
|
+
"y": 10
|
|
3419
|
+
},
|
|
3420
|
+
{
|
|
3421
|
+
"id": "app",
|
|
3422
|
+
"label": "App servers: metadata API",
|
|
3423
|
+
"x": 230,
|
|
3424
|
+
"y": 126
|
|
3425
|
+
},
|
|
3426
|
+
{
|
|
3427
|
+
"id": "s-meta",
|
|
3428
|
+
"label": "?",
|
|
3429
|
+
"x": 480,
|
|
3430
|
+
"y": 126
|
|
3431
|
+
},
|
|
3432
|
+
{
|
|
3433
|
+
"id": "s-notify",
|
|
3434
|
+
"label": "?",
|
|
3435
|
+
"x": 230,
|
|
3436
|
+
"y": 242
|
|
3437
|
+
}
|
|
3438
|
+
],
|
|
3439
|
+
"edges": [
|
|
3440
|
+
[
|
|
3441
|
+
"apps",
|
|
3442
|
+
"app",
|
|
3443
|
+
"metadata"
|
|
3444
|
+
],
|
|
3445
|
+
[
|
|
3446
|
+
"apps",
|
|
3447
|
+
"s-blob",
|
|
3448
|
+
"upload chunks"
|
|
3449
|
+
],
|
|
3450
|
+
[
|
|
3451
|
+
"s-blob",
|
|
3452
|
+
"s-cdn"
|
|
3453
|
+
],
|
|
3454
|
+
[
|
|
3455
|
+
"s-cdn",
|
|
3456
|
+
"apps",
|
|
3457
|
+
"download"
|
|
3458
|
+
],
|
|
3459
|
+
[
|
|
3460
|
+
"app",
|
|
3461
|
+
"s-meta",
|
|
3462
|
+
"files, versions"
|
|
3463
|
+
],
|
|
3464
|
+
[
|
|
3465
|
+
"app",
|
|
3466
|
+
"s-notify"
|
|
3467
|
+
],
|
|
3468
|
+
[
|
|
3469
|
+
"s-notify",
|
|
3470
|
+
"apps",
|
|
3471
|
+
"nudge"
|
|
3472
|
+
]
|
|
3473
|
+
]
|
|
3474
|
+
},
|
|
3475
|
+
"slots": {
|
|
3476
|
+
"s-blob": "Object storage: chunks",
|
|
3477
|
+
"s-meta": "Metadata database",
|
|
3478
|
+
"s-cdn": "CDN",
|
|
3479
|
+
"s-notify": "Notification service"
|
|
3480
|
+
},
|
|
3481
|
+
"palette": [
|
|
3482
|
+
"Object storage: chunks",
|
|
3483
|
+
"Metadata database",
|
|
3484
|
+
"CDN",
|
|
3485
|
+
"Notification service",
|
|
3486
|
+
"Database: file bytes as blobs",
|
|
3487
|
+
"Nightly sync job",
|
|
3488
|
+
"Search index"
|
|
3489
|
+
],
|
|
3490
|
+
"explain": "Chunks go up to object storage on presigned URLs and come back through the CDN, so app servers handle only small metadata calls. The metadata database records files, versions and chunk lists: it is the truth sync reads. A notification service nudges other devices to ask what changed. Bytes bloat a database; a nightly job is too slow. Say: \"Bytes to storage, truth in metadata, a nudge to sync.\"",
|
|
3491
|
+
"source": "https://www.hellointerview.com/learn/system-design/problem-breakdowns/dropbox"
|
|
3492
|
+
},
|
|
3493
|
+
{
|
|
3494
|
+
"id": "fs-05",
|
|
3495
|
+
"type": "duel",
|
|
3496
|
+
"bloom": "evaluate",
|
|
3497
|
+
"prompt": "A laptop and a phone both edit the same document offline, each starting from version 7. The laptop syncs first and makes version 8. Then the phone syncs. What happens to the phone's edit?",
|
|
3498
|
+
"choices": [
|
|
3499
|
+
"Keep both: save it as a conflicted copy next to the file",
|
|
3500
|
+
"Last write wins: it replaces version 8",
|
|
3501
|
+
"Merge the two versions' bytes automatically",
|
|
3502
|
+
"Reject it and drop the phone's changes"
|
|
3503
|
+
],
|
|
3504
|
+
"correct": 0,
|
|
3505
|
+
"reasons": [
|
|
3506
|
+
"The phone's commit was based on version 7, so the server can see both devices changed it",
|
|
3507
|
+
"Conflicted copies take less storage",
|
|
3508
|
+
"The later edit is always the one the person wanted",
|
|
3509
|
+
"Any two files can be merged byte by byte"
|
|
3510
|
+
],
|
|
3511
|
+
"reason": 0,
|
|
3512
|
+
"costs": [
|
|
3513
|
+
"The person compares the two copies and tidies up by hand",
|
|
3514
|
+
"Every edit needs a lock from the server first",
|
|
3515
|
+
"One device's work is silently lost"
|
|
3516
|
+
],
|
|
3517
|
+
"cost": 0,
|
|
3518
|
+
"explain": "Each commit names the version it started from. If the file has moved on since, both devices changed it: replacing one silently loses work, and an arbitrary file's bytes can't be merged safely. A conflicted copy loses nothing, and the person merges them. Locks fail when devices are offline. Say: \"Commit against a base version; on a clash, keep both.\"",
|
|
3519
|
+
"source": "https://en.wikipedia.org/wiki/File_synchronization"
|
|
3520
|
+
},
|
|
3521
|
+
{
|
|
3522
|
+
"id": "fs-06",
|
|
3523
|
+
"type": "multi",
|
|
3524
|
+
"bloom": "analyse",
|
|
3525
|
+
"prompt": "A small part of a 1 GB file changes on a laptop. The phone should show it within seconds, without wasting data or requests. Which of these help?",
|
|
3526
|
+
"options": [
|
|
3527
|
+
"Upload only the chunks whose hashes changed",
|
|
3528
|
+
"Skip any chunk whose hash storage already holds",
|
|
3529
|
+
"The phone asks for every change since its saved cursor",
|
|
3530
|
+
"A long poll or WebSocket nudge tells the phone when to ask",
|
|
3531
|
+
"Re-upload the whole file, compressed",
|
|
3532
|
+
"The phone lists the whole folder every second",
|
|
3533
|
+
"Send the file's bytes inside the nudge",
|
|
3534
|
+
"Compare each file's modified time with the phone's clock"
|
|
3535
|
+
],
|
|
3536
|
+
"correct": [
|
|
3537
|
+
0,
|
|
3538
|
+
1,
|
|
3539
|
+
2,
|
|
3540
|
+
3
|
|
3541
|
+
],
|
|
3542
|
+
"explain": "Chunk hashes show what changed, and skipping hashes storage already holds removes duplicates, so a small edit moves a few MB, not 1 GB. The metadata database logs changes in order; the phone keeps a cursor into that log, and a nudge says when to read it. Polling wastes requests, nudges should stay small, and device clocks drift. Say: \"Changed chunks up, a nudge, then changes since my cursor.\"",
|
|
3543
|
+
"source": "https://www.hellointerview.com/learn/system-design/problem-breakdowns/dropbox"
|
|
3544
|
+
},
|
|
3545
|
+
{
|
|
3546
|
+
"id": "fs-boss",
|
|
3547
|
+
"type": "flaw",
|
|
3548
|
+
"bloom": "evaluate",
|
|
3549
|
+
"prompt": "Big uploads on patchy Wi-Fi fail near the end and start again from zero, and the app servers slow down whenever people upload. Find the box to change, then pick the fix.",
|
|
3550
|
+
"diagram": {
|
|
3551
|
+
"width": 640,
|
|
3552
|
+
"height": 300,
|
|
3553
|
+
"nodes": [
|
|
3554
|
+
{
|
|
3555
|
+
"id": "apps",
|
|
3556
|
+
"label": "Desktop and phone apps",
|
|
3557
|
+
"x": 10,
|
|
3558
|
+
"y": 126
|
|
3559
|
+
},
|
|
3560
|
+
{
|
|
3561
|
+
"id": "cdn",
|
|
3562
|
+
"label": "CDN",
|
|
3563
|
+
"x": 230,
|
|
3564
|
+
"y": 10
|
|
3565
|
+
},
|
|
3566
|
+
{
|
|
3567
|
+
"id": "storage",
|
|
3568
|
+
"label": "Object storage",
|
|
3569
|
+
"x": 480,
|
|
3570
|
+
"y": 10
|
|
3571
|
+
},
|
|
3572
|
+
{
|
|
3573
|
+
"id": "app",
|
|
3574
|
+
"label": "App servers",
|
|
3575
|
+
"x": 230,
|
|
3576
|
+
"y": 126
|
|
3577
|
+
},
|
|
3578
|
+
{
|
|
3579
|
+
"id": "meta",
|
|
3580
|
+
"label": "Metadata database",
|
|
3581
|
+
"x": 480,
|
|
3582
|
+
"y": 126
|
|
3583
|
+
},
|
|
3584
|
+
{
|
|
3585
|
+
"id": "notify",
|
|
3586
|
+
"label": "Notification service",
|
|
3587
|
+
"x": 230,
|
|
3588
|
+
"y": 242
|
|
3589
|
+
}
|
|
3590
|
+
],
|
|
3591
|
+
"edges": [
|
|
3592
|
+
[
|
|
3593
|
+
"apps",
|
|
3594
|
+
"app",
|
|
3595
|
+
"whole file"
|
|
3596
|
+
],
|
|
3597
|
+
[
|
|
3598
|
+
"app",
|
|
3599
|
+
"storage",
|
|
3600
|
+
"write file"
|
|
3601
|
+
],
|
|
3602
|
+
[
|
|
3603
|
+
"storage",
|
|
3604
|
+
"cdn"
|
|
3605
|
+
],
|
|
3606
|
+
[
|
|
3607
|
+
"cdn",
|
|
3608
|
+
"apps",
|
|
3609
|
+
"download"
|
|
3610
|
+
],
|
|
3611
|
+
[
|
|
3612
|
+
"app",
|
|
3613
|
+
"meta",
|
|
3614
|
+
"versions"
|
|
3615
|
+
],
|
|
3616
|
+
[
|
|
3617
|
+
"app",
|
|
3618
|
+
"notify"
|
|
3619
|
+
],
|
|
3620
|
+
[
|
|
3621
|
+
"notify",
|
|
3622
|
+
"apps",
|
|
3623
|
+
"nudge"
|
|
3624
|
+
]
|
|
3625
|
+
]
|
|
3626
|
+
},
|
|
3627
|
+
"flaw": "app",
|
|
3628
|
+
"fixes": [
|
|
3629
|
+
"Chunk files; send missing chunks straight to storage on presigned URLs, then commit",
|
|
3630
|
+
"Add more app servers and raise the upload timeout",
|
|
3631
|
+
"Compress each file before it's sent",
|
|
3632
|
+
"Save the files in the metadata database instead"
|
|
3633
|
+
],
|
|
3634
|
+
"fix": 0,
|
|
3635
|
+
"explain": "Relaying every byte through app servers ties them up for minutes per upload, and one dropped connection loses the whole file. Split files into chunks of a few MB: the server hands out presigned URLs only for chunks storage lacks, the app sends them straight there and retries just a failed one, then a commit makes the version live. Say: \"Chunks straight to storage, resume per chunk, then commit.\"",
|
|
3636
|
+
"source": "https://docs.aws.amazon.com/AmazonS3/latest/userguide/mpuoverview.html"
|
|
3637
|
+
}
|
|
3638
|
+
]
|
|
3639
|
+
},
|
|
3640
|
+
{
|
|
3641
|
+
"chapter": 12,
|
|
3642
|
+
"slug": "nearby-places",
|
|
3643
|
+
"kind": "quest",
|
|
3644
|
+
"title": "Nearby places (like Yelp)",
|
|
3645
|
+
"emoji": "📍",
|
|
3646
|
+
"pillar": "Quests",
|
|
3647
|
+
"builds_on": [
|
|
3648
|
+
1,
|
|
3649
|
+
4,
|
|
3650
|
+
5,
|
|
3651
|
+
8
|
|
3652
|
+
],
|
|
3653
|
+
"blurb": "Find good places near you by distance, type and words, and post reviews. Requirements, numbers, endpoints, the design, then the deep dives.",
|
|
3654
|
+
"boss": "np-boss",
|
|
3655
|
+
"challenges": [
|
|
3656
|
+
{
|
|
3657
|
+
"id": "np-01",
|
|
3658
|
+
"type": "sprint",
|
|
3659
|
+
"bloom": "create",
|
|
3660
|
+
"minutes": 10,
|
|
3661
|
+
"prompt": "Requirements for a nearby-places app (people find businesses near them by distance, type and words, read ratings and post reviews). Out loud, against the clock: read it back, who uses it and what they do, your three hard parts, how busy (with the peak), what can't break, what's out.",
|
|
3662
|
+
"checks": [
|
|
3663
|
+
"Read the problem back in one line",
|
|
3664
|
+
"Named who uses it: people searching nearby, people posting reviews, and owners keeping a place's details current",
|
|
3665
|
+
"Picked my own three hard parts, such as fast search by distance and words, ratings that stay current, and one review per person",
|
|
3666
|
+
"Asked for the scale and the peak, and said searches will far outnumber reviews",
|
|
3667
|
+
"Said what can't break: search stays fast and up, and a posted review is never lost; results may lag a little",
|
|
3668
|
+
"Said \"I'll assume…\" for scope, instead of asking \"do you want…?\"",
|
|
3669
|
+
"Said what's out of scope, such as bookings, photos or personalised ranking",
|
|
3670
|
+
"Finished inside the time"
|
|
3671
|
+
],
|
|
3672
|
+
"explain": "The first quarter shapes everything after it. Searches dwarf reviews and places hardly ever move, so the design leans on an index built for reading and on caching. Name that ratio out loud with your numbers. If a check missed, rehearse that line before the next round. Say: \"Read-heavy: searches dwarf reviews, and places rarely change.\"",
|
|
3673
|
+
"source": "https://www.hellointerview.com/learn/system-design/problem-breakdowns/yelp"
|
|
3674
|
+
},
|
|
3675
|
+
{
|
|
3676
|
+
"id": "np-02",
|
|
3677
|
+
"type": "estimate",
|
|
3678
|
+
"bloom": "apply",
|
|
3679
|
+
"prompt": "Searches on a popular nearby-places app. Estimate the busiest second.",
|
|
3680
|
+
"generator": {
|
|
3681
|
+
"kind": "peak_rps",
|
|
3682
|
+
"params": {
|
|
3683
|
+
"users": [
|
|
3684
|
+
5000000,
|
|
3685
|
+
50000000
|
|
3686
|
+
],
|
|
3687
|
+
"actions": [
|
|
3688
|
+
2,
|
|
3689
|
+
10
|
|
3690
|
+
]
|
|
3691
|
+
}
|
|
3692
|
+
},
|
|
3693
|
+
"explain": "Searches a day, divided by about 100,000 seconds, times 10 for the peak. Reviews are a small share of that and places barely change, so this number sizes the search index, the cache and the servers, not the database's writes. Round hard and say the working aloud. Say: \"Searches over 100,000, times ten: that's the peak, and nearly all of it is reads.\"",
|
|
3694
|
+
"source": "https://www.hellointerview.com/learn/system-design/in-a-hurry/core-concepts"
|
|
3695
|
+
},
|
|
3696
|
+
{
|
|
3697
|
+
"id": "np-03",
|
|
3698
|
+
"type": "write",
|
|
3699
|
+
"bloom": "apply",
|
|
3700
|
+
"prompt": "Name the core entities, then sketch the API (application programming interface): search near a point by radius, category and words, with paging; get a place's details; post a review.",
|
|
3701
|
+
"key_points": [
|
|
3702
|
+
"Entities: a place (name, location, category, average rating, review count), a review (place, user, stars, text) and a user",
|
|
3703
|
+
"GET /places/search with latitude, longitude, radius, category, words, a limit and a cursor; returns a page and the next cursor",
|
|
3704
|
+
"GET /places/{id} returns the details, the rating and the latest reviews",
|
|
3705
|
+
"POST /places/{id}/reviews with stars and text; the user comes from the token",
|
|
3706
|
+
"One review per user per place, enforced by the database; a second one returns 409 Conflict",
|
|
3707
|
+
"Results page with a cursor, so deep pages stay cheap and nothing repeats"
|
|
3708
|
+
],
|
|
3709
|
+
"model": "Entities: Place (id, name, location, category, average rating, review count), Review (place, user, stars, text, created at) and User. GET /places/search?lat=&lng=&radius=&category=&q=&limit=20&cursor= returns a page of places and nextCursor. GET /places/{id} returns the details, the rating and recent reviews. POST /places/{id}/reviews takes stars and text; the user comes from the token, and a second review of the same place returns 409.",
|
|
3710
|
+
"explain": "Three verbs carry this product, and one of them is nearly all the traffic: search. Put every filter on that one endpoint and page it with a cursor, so a long scroll stays cheap. Keep the rating on the place, so a search never adds up reviews. Say: \"Entities first; search is the hot endpoint, and reviews are the rare write.\"",
|
|
3711
|
+
"source": "https://www.hellointerview.com/learn/system-design/core-concepts/api-design"
|
|
3712
|
+
},
|
|
3713
|
+
{
|
|
3714
|
+
"id": "np-04",
|
|
3715
|
+
"type": "fill",
|
|
3716
|
+
"bloom": "apply",
|
|
3717
|
+
"prompt": "Land the design end to end. Drag each part into its box: what answers \"near here\" with filters, the source of truth, how those two stay in step, and what saves repeat work in busy areas.",
|
|
3718
|
+
"diagram": {
|
|
3719
|
+
"width": 640,
|
|
3720
|
+
"height": 300,
|
|
3721
|
+
"nodes": [
|
|
3722
|
+
{
|
|
3723
|
+
"id": "apps",
|
|
3724
|
+
"label": "Mobile and web apps",
|
|
3725
|
+
"x": 10,
|
|
3726
|
+
"y": 126
|
|
3727
|
+
},
|
|
3728
|
+
{
|
|
3729
|
+
"id": "search",
|
|
3730
|
+
"label": "Search service",
|
|
3731
|
+
"x": 160,
|
|
3732
|
+
"y": 40
|
|
3733
|
+
},
|
|
3734
|
+
{
|
|
3735
|
+
"id": "writes",
|
|
3736
|
+
"label": "Place and review service",
|
|
3737
|
+
"x": 160,
|
|
3738
|
+
"y": 212
|
|
3739
|
+
},
|
|
3740
|
+
{
|
|
3741
|
+
"id": "s-cache",
|
|
3742
|
+
"label": "?",
|
|
3743
|
+
"x": 380,
|
|
3744
|
+
"y": 10
|
|
3745
|
+
},
|
|
3746
|
+
{
|
|
3747
|
+
"id": "s-index",
|
|
3748
|
+
"label": "?",
|
|
3749
|
+
"x": 380,
|
|
3750
|
+
"y": 126
|
|
3751
|
+
},
|
|
3752
|
+
{
|
|
3753
|
+
"id": "s-db",
|
|
3754
|
+
"label": "?",
|
|
3755
|
+
"x": 330,
|
|
3756
|
+
"y": 242
|
|
3757
|
+
},
|
|
3758
|
+
{
|
|
3759
|
+
"id": "s-feed",
|
|
3760
|
+
"label": "?",
|
|
3761
|
+
"x": 510,
|
|
3762
|
+
"y": 242
|
|
3763
|
+
}
|
|
3764
|
+
],
|
|
3765
|
+
"edges": [
|
|
3766
|
+
[
|
|
3767
|
+
"apps",
|
|
3768
|
+
"search"
|
|
3769
|
+
],
|
|
3770
|
+
[
|
|
3771
|
+
"apps",
|
|
3772
|
+
"writes"
|
|
3773
|
+
],
|
|
3774
|
+
[
|
|
3775
|
+
"search",
|
|
3776
|
+
"s-cache",
|
|
3777
|
+
"by cell"
|
|
3778
|
+
],
|
|
3779
|
+
[
|
|
3780
|
+
"search",
|
|
3781
|
+
"s-index",
|
|
3782
|
+
"on a miss"
|
|
3783
|
+
],
|
|
3784
|
+
[
|
|
3785
|
+
"writes",
|
|
3786
|
+
"s-db",
|
|
3787
|
+
"save"
|
|
3788
|
+
],
|
|
3789
|
+
[
|
|
3790
|
+
"s-db",
|
|
3791
|
+
"s-feed"
|
|
3792
|
+
],
|
|
3793
|
+
[
|
|
3794
|
+
"s-feed",
|
|
3795
|
+
"s-index"
|
|
3796
|
+
]
|
|
3797
|
+
]
|
|
3798
|
+
},
|
|
3799
|
+
"slots": {
|
|
3800
|
+
"s-cache": "Cache of results by cell",
|
|
3801
|
+
"s-index": "Geo search index",
|
|
3802
|
+
"s-db": "Database: places, reviews",
|
|
3803
|
+
"s-feed": "Change data capture"
|
|
3804
|
+
},
|
|
3805
|
+
"palette": [
|
|
3806
|
+
"Cache of results by cell",
|
|
3807
|
+
"Geo search index",
|
|
3808
|
+
"Database: places, reviews",
|
|
3809
|
+
"Change data capture",
|
|
3810
|
+
"WebSocket server",
|
|
3811
|
+
"Graph database",
|
|
3812
|
+
"Blob storage"
|
|
3813
|
+
],
|
|
3814
|
+
"explain": "Search checks a cache keyed by cell and filters, and on a miss asks the geo search index, which applies distance, category and words in one query. The database is the truth for places and reviews; change data capture (CDC) streams each change into the index. Say: \"The database is the truth, a change feed keeps the geo index current, and busy cells are cached.\"",
|
|
3815
|
+
"source": "https://www.hellointerview.com/learn/system-design/deep-dives/elasticsearch"
|
|
3816
|
+
},
|
|
3817
|
+
{
|
|
3818
|
+
"id": "np-05",
|
|
3819
|
+
"type": "duel",
|
|
3820
|
+
"bloom": "evaluate",
|
|
3821
|
+
"prompt": "Deep dive: say a city-centre square kilometre holds 5,000 places, while a 20 km square in the hills holds a dozen. You want every search to read a similar, small number of places. How do you divide the map into cells?",
|
|
3822
|
+
"choices": [
|
|
3823
|
+
"A quadtree: split any cell holding more than about 100 places into four",
|
|
3824
|
+
"Equal cells everywhere, sized for the city centre",
|
|
3825
|
+
"Equal cells everywhere, sized for the countryside"
|
|
3826
|
+
],
|
|
3827
|
+
"correct": 0,
|
|
3828
|
+
"reasons": [
|
|
3829
|
+
"Cells shrink where places crowd together and stay large where they're sparse",
|
|
3830
|
+
"Equal cells make every search read the same number of places",
|
|
3831
|
+
"Small cells mean a search never needs the cells around it"
|
|
3832
|
+
],
|
|
3833
|
+
"reason": 0,
|
|
3834
|
+
"costs": [
|
|
3835
|
+
"More to build and keep current: cells split as places are added",
|
|
3836
|
+
"Every search has to read every place in the city",
|
|
3837
|
+
"Results can no longer be sorted by distance"
|
|
3838
|
+
],
|
|
3839
|
+
"cost": 0,
|
|
3840
|
+
"explain": "Equal cells sized for the centre make a countryside search open many near-empty cells; sized for the countryside, one city cell holds thousands. A quadtree splits only crowded cells, so no leaf holds more than a set number. Either way, read the user's cell and its neighbours, then keep places within the exact distance. Say: \"Quadtree for uneven density; cell plus neighbours, then exact distance.\"",
|
|
3841
|
+
"source": "https://en.wikipedia.org/wiki/Quadtree"
|
|
3842
|
+
},
|
|
3843
|
+
{
|
|
3844
|
+
"id": "np-06",
|
|
3845
|
+
"type": "multi",
|
|
3846
|
+
"bloom": "analyse",
|
|
3847
|
+
"prompt": "Deep dive: someone posts a review, and that place's rating shows on every search result it appears in. Each person may review a place only once. Which of these belong in the design?",
|
|
3848
|
+
"options": [
|
|
3849
|
+
"A unique constraint on user and place together, so a second review is refused",
|
|
3850
|
+
"A review count and star total on the place, updated in the same transaction",
|
|
3851
|
+
"An edit swaps the old stars for the new ones in the place's total",
|
|
3852
|
+
"Average all of a place's reviews each time it appears in a search",
|
|
3853
|
+
"Check in code for an earlier review, then insert, with no constraint",
|
|
3854
|
+
"Let the app send the place's new average along with the review"
|
|
3855
|
+
],
|
|
3856
|
+
"correct": [
|
|
3857
|
+
0,
|
|
3858
|
+
1,
|
|
3859
|
+
2
|
|
3860
|
+
],
|
|
3861
|
+
"explain": "Two requests from one user at once can both pass a check-then-insert, so only a unique constraint truly stops a second review. A count and a total on the place, changed with the review, make each rating on a results page one read; a burst on one place can be folded in small batches. Never trust a sum sent by the app. Say: \"Unique on user and place; the rating is precomputed on write.\"",
|
|
3862
|
+
"source": "https://www.postgresql.org/docs/current/ddl-constraints.html"
|
|
3863
|
+
},
|
|
3864
|
+
{
|
|
3865
|
+
"id": "np-boss",
|
|
3866
|
+
"type": "duel",
|
|
3867
|
+
"bloom": "evaluate",
|
|
3868
|
+
"prompt": "Deep dive: how does search answer \"vegan brunch, cafés, within 2 km\" across 10 million places, fast, with distance, category and words all applied?",
|
|
3869
|
+
"choices": [
|
|
3870
|
+
"A search index with geo queries, such as Elasticsearch, fed from the database",
|
|
3871
|
+
"The places table, with separate latitude and longitude indexes, and LIKE for words",
|
|
3872
|
+
"Load every place into each server's memory and measure the distance to each"
|
|
3873
|
+
],
|
|
3874
|
+
"correct": 0,
|
|
3875
|
+
"reasons": [
|
|
3876
|
+
"One query narrows by area, category and words together, touching only nearby matches",
|
|
3877
|
+
"Two separate indexes together pick out a circle on the map",
|
|
3878
|
+
"Places rarely change, so measuring all of them is cheap"
|
|
3879
|
+
],
|
|
3880
|
+
"reason": 0,
|
|
3881
|
+
"costs": [
|
|
3882
|
+
"A second copy of the data to keep in step, so changes appear after a short lag",
|
|
3883
|
+
"Every search reads every place on Earth",
|
|
3884
|
+
"Results can no longer be sorted by rating"
|
|
3885
|
+
],
|
|
3886
|
+
"cost": 0,
|
|
3887
|
+
"explain": "Separate latitude and longitude indexes each match a strip across the whole map, and LIKE with a leading wildcard can't use a normal index, so both read far too many rows. A geo search index applies distance, category and words in one query, kept in step by CDC. PostGIS with full-text search does the same inside PostgreSQL. Say: \"A geo search index fed by the database: one query, every filter.\"",
|
|
3888
|
+
"source": "https://www.hellointerview.com/learn/system-design/deep-dives/elasticsearch"
|
|
3889
|
+
}
|
|
3890
|
+
]
|
|
3891
|
+
},
|
|
3892
|
+
{
|
|
3893
|
+
"chapter": 13,
|
|
3894
|
+
"slug": "ride-booking",
|
|
3895
|
+
"kind": "quest",
|
|
3896
|
+
"title": "Ride booking (like Uber)",
|
|
3897
|
+
"emoji": "🚗",
|
|
3898
|
+
"pillar": "Quests",
|
|
3899
|
+
"builds_on": [
|
|
3900
|
+
1,
|
|
3901
|
+
3,
|
|
3902
|
+
6,
|
|
3903
|
+
8
|
|
3904
|
+
],
|
|
3905
|
+
"blurb": "Pair riders with nearby drivers in seconds. Requirements, numbers, entities and calls, the design, then live location, surge pricing and one-to-one matching.",
|
|
3906
|
+
"boss": "rb-boss",
|
|
3907
|
+
"challenges": [
|
|
3908
|
+
{
|
|
3909
|
+
"id": "rb-01",
|
|
3910
|
+
"type": "sprint",
|
|
3911
|
+
"bloom": "create",
|
|
3912
|
+
"minutes": 10,
|
|
3913
|
+
"prompt": "Requirements for a ride-booking app (a rider asks for a car, a nearby driver accepts, and both follow the trip live). Out loud, against the clock: read it back, both kinds of user and what they do, your three hard parts, how busy (with the peak), what can't break, what's out.",
|
|
3914
|
+
"checks": [
|
|
3915
|
+
"Read the problem back in one line",
|
|
3916
|
+
"Named both users: riders who get a price, book and track a ride, and drivers who go online, accept and drive",
|
|
3917
|
+
"Picked my own three hard parts, such as constant location updates, one-to-one matching, and rush-hour peaks",
|
|
3918
|
+
"Gave the scale and the peak: drivers online, a ping every few seconds, rush hour and big events",
|
|
3919
|
+
"Said what can't break: one driver per ride, one ride per driver, and the rider charged once",
|
|
3920
|
+
"Said \"I'll assume…\" for scope, instead of asking \"do you want…?\"",
|
|
3921
|
+
"Said what's out of scope, such as shared rides, booking ahead or signing drivers up",
|
|
3922
|
+
"Finished inside the time"
|
|
3923
|
+
],
|
|
3924
|
+
"explain": "Ride booking has two live sides: drivers report their position all the time, and riders expect a match within seconds. Naming that early, with peaks by area and time, sets up every later choice. Correctness lives in matching and payment, so say those out loud. If a check missed, rehearse that line before the next round. Say: \"Two live sides, constant location, and one driver per ride.\"",
|
|
3925
|
+
"source": "https://www.hellointerview.com/learn/system-design/problem-breakdowns/uber"
|
|
3926
|
+
},
|
|
3927
|
+
{
|
|
3928
|
+
"id": "rb-02",
|
|
3929
|
+
"type": "estimate",
|
|
3930
|
+
"bloom": "apply",
|
|
3931
|
+
"prompt": "Drivers' phones send their location every few seconds through a shift. Estimate the location writes a second at the peak, then say whether one database copes.",
|
|
3932
|
+
"generator": {
|
|
3933
|
+
"kind": "shard_or_not",
|
|
3934
|
+
"params": {
|
|
3935
|
+
"users": [
|
|
3936
|
+
100000,
|
|
3937
|
+
2000000
|
|
3938
|
+
],
|
|
3939
|
+
"writes": [
|
|
3940
|
+
2000,
|
|
3941
|
+
10000
|
|
3942
|
+
]
|
|
3943
|
+
}
|
|
3944
|
+
},
|
|
3945
|
+
"explain": "Drivers × pings a day, divided by about 100,000 seconds, times 10 for the peak. A ping every 5 seconds over 8 hours is about 5,760 a day, so even 100,000 drivers pass 10,000 writes a second at the peak. Each point is stale seconds later, so don't keep every ping: hold each driver's latest point in memory, split by area. Say: \"Hot, short-lived writes, so memory, not the main database.\"",
|
|
3946
|
+
"source": "https://www.hellointerview.com/learn/system-design/problem-breakdowns/uber"
|
|
3947
|
+
},
|
|
3948
|
+
{
|
|
3949
|
+
"id": "rb-03",
|
|
3950
|
+
"type": "write",
|
|
3951
|
+
"bloom": "apply",
|
|
3952
|
+
"prompt": "Name the core entities, then sketch the application programming interface (API): get a fare estimate, request a ride, send a driver's location, accept or decline an offer, and follow the ride's status.",
|
|
3953
|
+
"key_points": [
|
|
3954
|
+
"Entities: rider, driver, ride (pickup, drop-off, fare, driver, status) and each driver's latest location",
|
|
3955
|
+
"POST /fares with pickup and drop-off returns a price and a quote ID that expires after a few minutes",
|
|
3956
|
+
"POST /rides with the quote ID creates the ride as requested and starts matching",
|
|
3957
|
+
"POST /drivers/location every few seconds; POST /rides/{id}/accept or /decline answers an offer",
|
|
3958
|
+
"GET /rides/{id} gives the status, and changes are pushed over a WebSocket or server-sent events (SSE)",
|
|
3959
|
+
"Rider and driver come from the token, never the request body"
|
|
3960
|
+
],
|
|
3961
|
+
"model": "Entities: Rider, Driver, Ride (pickup, drop-off, quote, driver, status) and Location (driver, point, time). POST /fares returns a price and a quote ID. POST /rides with that ID creates a requested ride. The driver's app sends POST /drivers/location every few seconds and answers offers with POST /rides/{id}/accept or /decline. GET /rides/{id} returns the status, also pushed live. Rider and driver come from the token.",
|
|
3962
|
+
"explain": "Five calls carry the product. Name the ride's states early: requested, matched, driver arriving, in progress, then completed or cancelled. Payment runs once, on completed, with the ride ID as an idempotency key so a retry can't charge twice. Taking the person from the token stops one user acting as another. Say: \"Entities first; the ride's status is a state machine.\"",
|
|
3963
|
+
"source": "https://www.hellointerview.com/learn/system-design/problem-breakdowns/uber"
|
|
3964
|
+
},
|
|
3965
|
+
{
|
|
3966
|
+
"id": "rb-04",
|
|
3967
|
+
"type": "fill",
|
|
3968
|
+
"bloom": "apply",
|
|
3969
|
+
"prompt": "Land the design end to end. Drag each part into its box: what pairs riders with drivers, where drivers' latest positions live, the record of every ride, and how the rider's screen stays current.",
|
|
3970
|
+
"diagram": {
|
|
3971
|
+
"width": 640,
|
|
3972
|
+
"height": 300,
|
|
3973
|
+
"nodes": [
|
|
3974
|
+
{
|
|
3975
|
+
"id": "driver",
|
|
3976
|
+
"label": "Driver app",
|
|
3977
|
+
"x": 10,
|
|
3978
|
+
"y": 20
|
|
3979
|
+
},
|
|
3980
|
+
{
|
|
3981
|
+
"id": "loc",
|
|
3982
|
+
"label": "Location service",
|
|
3983
|
+
"x": 260,
|
|
3984
|
+
"y": 20
|
|
3985
|
+
},
|
|
3986
|
+
{
|
|
3987
|
+
"id": "s-geo",
|
|
3988
|
+
"label": "?",
|
|
3989
|
+
"x": 510,
|
|
3990
|
+
"y": 126
|
|
3991
|
+
},
|
|
3992
|
+
{
|
|
3993
|
+
"id": "rider",
|
|
3994
|
+
"label": "Rider app",
|
|
3995
|
+
"x": 10,
|
|
3996
|
+
"y": 126
|
|
3997
|
+
},
|
|
3998
|
+
{
|
|
3999
|
+
"id": "s-match",
|
|
4000
|
+
"label": "?",
|
|
4001
|
+
"x": 260,
|
|
4002
|
+
"y": 126
|
|
4003
|
+
},
|
|
4004
|
+
{
|
|
4005
|
+
"id": "s-live",
|
|
4006
|
+
"label": "?",
|
|
4007
|
+
"x": 10,
|
|
4008
|
+
"y": 232
|
|
4009
|
+
},
|
|
4010
|
+
{
|
|
4011
|
+
"id": "s-db",
|
|
4012
|
+
"label": "?",
|
|
4013
|
+
"x": 260,
|
|
4014
|
+
"y": 232
|
|
4015
|
+
}
|
|
4016
|
+
],
|
|
4017
|
+
"edges": [
|
|
4018
|
+
[
|
|
4019
|
+
"driver",
|
|
4020
|
+
"loc",
|
|
4021
|
+
"ping"
|
|
4022
|
+
],
|
|
4023
|
+
[
|
|
4024
|
+
"loc",
|
|
4025
|
+
"s-geo",
|
|
4026
|
+
"latest point"
|
|
4027
|
+
],
|
|
4028
|
+
[
|
|
4029
|
+
"rider",
|
|
4030
|
+
"s-match",
|
|
4031
|
+
"request ride"
|
|
4032
|
+
],
|
|
4033
|
+
[
|
|
4034
|
+
"s-match",
|
|
4035
|
+
"s-geo",
|
|
4036
|
+
"nearest free"
|
|
4037
|
+
],
|
|
4038
|
+
[
|
|
4039
|
+
"s-match",
|
|
4040
|
+
"driver",
|
|
4041
|
+
"offer"
|
|
4042
|
+
],
|
|
4043
|
+
[
|
|
4044
|
+
"s-match",
|
|
4045
|
+
"s-db"
|
|
4046
|
+
],
|
|
4047
|
+
[
|
|
4048
|
+
"s-match",
|
|
4049
|
+
"s-live",
|
|
4050
|
+
"status change"
|
|
4051
|
+
],
|
|
4052
|
+
[
|
|
4053
|
+
"s-live",
|
|
4054
|
+
"rider"
|
|
4055
|
+
]
|
|
4056
|
+
]
|
|
4057
|
+
},
|
|
4058
|
+
"slots": {
|
|
4059
|
+
"s-match": "Matching service",
|
|
4060
|
+
"s-geo": "In-memory geo index",
|
|
4061
|
+
"s-db": "Rides database",
|
|
4062
|
+
"s-live": "Live updates (WebSocket or SSE)"
|
|
4063
|
+
},
|
|
4064
|
+
"palette": [
|
|
4065
|
+
"Matching service",
|
|
4066
|
+
"In-memory geo index",
|
|
4067
|
+
"Rides database",
|
|
4068
|
+
"Live updates (WebSocket or SSE)",
|
|
4069
|
+
"Message queue",
|
|
4070
|
+
"Data warehouse",
|
|
4071
|
+
"Content delivery network"
|
|
4072
|
+
],
|
|
4073
|
+
"explain": "Pings overwrite each driver's latest point in an in-memory geo index. The matching service asks it for the nearest free drivers, offers the ride to one, and keeps the ride and its state in the database. Status changes go out over a live channel, so the rider app never polls. Give each box its reason as you place it. Say: \"Location, matching, the ride record, live status.\"",
|
|
4074
|
+
"source": "https://www.hellointerview.com/learn/system-design/problem-breakdowns/uber"
|
|
4075
|
+
},
|
|
4076
|
+
{
|
|
4077
|
+
"id": "rb-05",
|
|
4078
|
+
"type": "duel",
|
|
4079
|
+
"bloom": "evaluate",
|
|
4080
|
+
"prompt": "Rush hour, and a concert has just ended: in one district, riders outnumber free drivers ten to one. Every fare quote must show that area's current surge multiplier. How do you produce it?",
|
|
4081
|
+
"choices": [
|
|
4082
|
+
"A job recomputes each area's multiplier every minute; quotes read it from a cache",
|
|
4083
|
+
"Count riders and free drivers in the area on every quote",
|
|
4084
|
+
"Set one multiplier per city each morning"
|
|
4085
|
+
],
|
|
4086
|
+
"correct": 0,
|
|
4087
|
+
"reasons": [
|
|
4088
|
+
"A quote becomes one cheap lookup, however many riders ask at once",
|
|
4089
|
+
"Counting on every quote costs nothing when the index is in memory",
|
|
4090
|
+
"A cached multiplier is exact to the second"
|
|
4091
|
+
],
|
|
4092
|
+
"reason": 0,
|
|
4093
|
+
"costs": [
|
|
4094
|
+
"The price trails demand by up to a minute",
|
|
4095
|
+
"Each quote searches the location index, so work grows with the crowd",
|
|
4096
|
+
"Riders can't see a price before booking"
|
|
4097
|
+
],
|
|
4098
|
+
"cost": 0,
|
|
4099
|
+
"explain": "Demand spikes by place and time, so counting supply and demand on every quote adds the most work just as the crowd arrives. Recompute each area's multiplier on a short schedule from recent requests and free drivers, and serve it as a cached read. Save it in the quote, so the rider pays the price they saw. Say: \"Surge is a precomputed read per area, refreshed every minute.\"",
|
|
4100
|
+
"source": "https://en.wikipedia.org/wiki/Dynamic_pricing"
|
|
4101
|
+
},
|
|
4102
|
+
{
|
|
4103
|
+
"id": "rb-06",
|
|
4104
|
+
"type": "multi",
|
|
4105
|
+
"bloom": "analyse",
|
|
4106
|
+
"prompt": "Drivers worldwide send their position every few seconds, and matching asks for free drivers near each pickup. Which of these keep the location index fast and accurate?",
|
|
4107
|
+
"options": [
|
|
4108
|
+
"Keep only each driver's latest point, overwriting the one before",
|
|
4109
|
+
"Treat a driver who hasn't pinged for about 30 seconds as gone",
|
|
4110
|
+
"Split the index by area, such as city or geohash cell, each shard serving its region",
|
|
4111
|
+
"Ping less often while a driver is parked, more often on the move",
|
|
4112
|
+
"Insert every ping as a new row in the rides database",
|
|
4113
|
+
"Cache each area's list of nearby drivers for an hour",
|
|
4114
|
+
"Run one global index on a single machine"
|
|
4115
|
+
],
|
|
4116
|
+
"correct": [
|
|
4117
|
+
0,
|
|
4118
|
+
1,
|
|
4119
|
+
2,
|
|
4120
|
+
3
|
|
4121
|
+
],
|
|
4122
|
+
"explain": "Only the newest point matters, so overwrite it in memory and let silent drivers drop out after a short expiry. Splitting by area keeps writes and searches local. Pinging less while parked cuts writes. A cached list goes stale as drivers move, a row per ping swamps the database, and one machine can't keep up. Say: \"Latest point, in memory, split by area, with an expiry.\"",
|
|
4123
|
+
"source": "https://www.hellointerview.com/learn/system-design/problem-breakdowns/uber"
|
|
4124
|
+
},
|
|
4125
|
+
{
|
|
4126
|
+
"id": "rb-boss",
|
|
4127
|
+
"type": "duel",
|
|
4128
|
+
"bloom": "evaluate",
|
|
4129
|
+
"prompt": "Deep dive: matching. Each driver sees one offer at a time, each ride goes to one driver at a time, and a driver has 10 seconds to answer. Many matching servers run at once. How do you make sure no driver gets two offers, and the ride keeps moving?",
|
|
4130
|
+
"choices": [
|
|
4131
|
+
"Lock the driver for 10 seconds before offering; on a no or a timeout, try the next nearest",
|
|
4132
|
+
"Offer the ride to the five nearest drivers and take the first to accept",
|
|
4133
|
+
"Check the driver's status says free, then send the offer"
|
|
4134
|
+
],
|
|
4135
|
+
"correct": 0,
|
|
4136
|
+
"reasons": [
|
|
4137
|
+
"Taking the lock is one atomic step, so only one server wins, and it frees itself",
|
|
4138
|
+
"Locks make drivers answer faster",
|
|
4139
|
+
"Checking the status first is already atomic"
|
|
4140
|
+
],
|
|
4141
|
+
"reason": 0,
|
|
4142
|
+
"costs": [
|
|
4143
|
+
"Each ignored offer costs the rider 10 seconds, and the lock store must stay up",
|
|
4144
|
+
"A driver can still get two offers at once",
|
|
4145
|
+
"Rides can no longer be cancelled"
|
|
4146
|
+
],
|
|
4147
|
+
"cost": 0,
|
|
4148
|
+
"explain": "Checking \"free\" then offering lets two servers pick the same driver. Claim the driver in one atomic step with a time limit: set a key only if none exists, expiring in 10 seconds. Accepting moves the ride from requested to matched with a conditional write, so it never gets two drivers. A no or a timeout frees the driver for the next offer. Say: \"An expiring lock per driver, one offer at a time.\"",
|
|
4149
|
+
"source": "https://redis.io/docs/latest/develop/clients/patterns/distributed-locks/"
|
|
4150
|
+
}
|
|
4151
|
+
]
|
|
4152
|
+
}
|
|
4153
|
+
]
|
|
4154
|
+
}
|