-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathEDA_Interview_Question_Bank_Complete.html
More file actions
378 lines (359 loc) · 133 KB
/
Copy pathEDA_Interview_Question_Bank_Complete.html
File metadata and controls
378 lines (359 loc) · 133 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>EDA Interview Question Bank — Complete</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<style>
:root{--bg:#0D1117;--surf:#161B22;--surf2:#1C2128;--surf3:#21262D;--bdr:#30363D;--bdr2:#3D444D;--tx:#E6EDF3;--tx2:#8B949E;--tx3:#484F58;--acc:#2F81F7;--acc-bg:rgba(47,129,247,.12);--acc-bdr:rgba(47,129,247,.35);--grn:#3FB950;--grn-bg:rgba(63,185,80,.1);--grn-bdr:rgba(63,185,80,.3);--amb:#D29922;--amb-bg:rgba(210,153,34,.1);--amb-bdr:rgba(210,153,34,.3);--red:#F85149;--red-bg:rgba(248,81,73,.1);--red-bdr:rgba(248,81,73,.3);--pur:#BC8CFF;--pur-bg:rgba(188,140,255,.1);--pur-bdr:rgba(188,140,255,.3);--mono:'JetBrains Mono',monospace;--sans:'Inter',sans-serif;--r:8px;--rl:12px}
*{box-sizing:border-box;margin:0;padding:0}
body{background:var(--bg);color:var(--tx);font-family:var(--sans);font-size:14px;line-height:1.6;min-height:100vh}
.hdr{background:var(--surf);border-bottom:1px solid var(--bdr);padding:0 24px;position:sticky;top:0;z-index:100;display:flex;align-items:center;gap:16px;height:56px}
.logo{font-family:var(--mono);font-size:13px;font-weight:500;color:var(--acc);background:var(--acc-bg);border:1px solid var(--acc-bdr);padding:3px 10px;border-radius:6px;flex-shrink:0}
.hdr-t{font-size:14px;font-weight:500}.hdr-s{font-size:12px;color:var(--tx2)}
.hdr-r{margin-left:auto;display:flex;align-items:center;gap:12px}
.hdr-link{font-size:12px;color:var(--tx2);text-decoration:none;white-space:nowrap}
.hdr-link:hover{color:var(--acc)}
.pw{display:flex;align-items:center;gap:8px}.pl{font-size:12px;color:var(--tx2)}
.pb{width:120px;height:4px;background:var(--surf3);border-radius:2px;overflow:hidden}
.pf{height:100%;background:var(--grn);border-radius:2px;transition:width .4s;width:0%}
.pc{font-size:12px;font-family:var(--mono);color:var(--grn);min-width:36px}
.layout{display:flex;min-height:calc(100vh - 56px)}
.sb{width:220px;flex-shrink:0;background:var(--surf);border-right:1px solid var(--bdr);padding:20px 0;position:sticky;top:56px;height:calc(100vh - 56px);overflow-y:auto}
.sbl{font-size:10px;font-weight:600;letter-spacing:.08em;color:var(--tx3);text-transform:uppercase;padding:0 16px;margin-bottom:4px;margin-top:16px;display:block}
.sbl:first-child{margin-top:0}
.sbi{display:flex;align-items:center;gap:8px;padding:6px 16px;cursor:pointer;color:var(--tx2);font-size:13px;border-left:2px solid transparent;width:100%;background:transparent;border-right:0;border-top:0;border-bottom:0;text-align:left;transition:all .12s}
.sbi:hover{background:var(--surf2);color:var(--tx)}.sbi.active{color:var(--acc);border-left-color:var(--acc);background:var(--acc-bg)}
.sdot{width:8px;height:8px;border-radius:50%;flex-shrink:0}
.scnt{margin-left:auto;font-size:11px;font-family:var(--mono);color:var(--tx3);background:var(--surf3);padding:1px 6px;border-radius:10px}
.main{flex:1;padding:24px;min-width:0;max-width:960px}
.stats{display:grid;grid-template-columns:repeat(4,1fr);gap:12px;margin-bottom:24px}
.stat{background:var(--surf);border:1px solid var(--bdr);border-radius:var(--rl);padding:14px 16px}
.stn{font-size:24px;font-weight:600;font-family:var(--mono);line-height:1}.stl{font-size:12px;color:var(--tx2);margin-top:4px}
.ctrls{display:flex;gap:10px;margin-bottom:16px;align-items:center;flex-wrap:wrap}
.sw{flex:1;min-width:180px;position:relative}
.sw svg{position:absolute;left:10px;top:50%;transform:translateY(-50%);color:var(--tx3);pointer-events:none}
.si{width:100%;background:var(--surf);border:1px solid var(--bdr);border-radius:var(--r);color:var(--tx);font-size:13px;padding:8px 12px 8px 34px;font-family:var(--sans);transition:border-color .15s}
.si:focus{outline:none;border-color:var(--acc)}.si::placeholder{color:var(--tx3)}
.cb{background:var(--surf);border:1px solid var(--bdr);border-radius:var(--r);color:var(--tx2);font-size:12px;padding:7px 12px;cursor:pointer;white-space:nowrap;font-family:var(--sans);transition:all .12s}
.cb:hover{border-color:var(--bdr2);color:var(--tx)}.cb.on{background:var(--acc-bg);border-color:var(--acc-bdr);color:var(--acc)}
.ql{display:flex;flex-direction:column;gap:8px}
.qc{background:var(--surf);border:1px solid var(--bdr);border-radius:var(--rl);overflow:hidden;transition:border-color .15s}
.qc:hover{border-color:var(--bdr2)}.qc.rev{border-left:3px solid var(--grn)}
.qh{display:flex;align-items:flex-start;gap:12px;padding:14px 16px;cursor:pointer;user-select:none}
.qh:hover{background:var(--surf2)}
.qn{font-family:var(--mono);font-size:11px;color:var(--tx3);min-width:28px;padding-top:3px;flex-shrink:0}
.qcnt{flex:1;min-width:0}.qt{font-size:14px;font-weight:500;line-height:1.4;margin-bottom:6px}
.qtags{display:flex;gap:5px;flex-wrap:wrap}
.tag{display:inline-block;font-size:11px;padding:2px 8px;border-radius:20px;font-weight:500;border:1px solid}
.t-delivery{background:var(--acc-bg);color:#58A6FF;border-color:var(--acc-bdr)}
.t-ordering{background:var(--amb-bg);color:var(--amb);border-color:var(--amb-bdr)}
.t-failure{background:var(--red-bg);color:var(--red);border-color:var(--red-bdr)}
.t-kafka{background:var(--grn-bg);color:var(--grn);border-color:var(--grn-bdr)}
.t-patterns{background:var(--surf3);color:var(--tx2);border-color:var(--bdr)}
.t-backpressure{background:rgba(227,179,65,.1);color:#E3B341;border-color:rgba(227,179,65,.3)}
.t-schema{background:var(--pur-bg);color:var(--pur);border-color:var(--pur-bdr)}
.t-observability{background:rgba(57,211,83,.1);color:#39D353;border-color:rgba(57,211,83,.3)}
.t-async{background:rgba(255,123,114,.08);color:#FF7B72;border-color:rgba(255,123,114,.25)}
.t-realworld{background:var(--red-bg);color:var(--red);border-color:var(--red-bdr)}
.t-hard{background:var(--red-bg);color:var(--red);border-color:var(--red-bdr)}
.t-med{background:var(--amb-bg);color:var(--amb);border-color:var(--amb-bdr)}
.t-rev{background:var(--grn-bg);color:var(--grn);border-color:var(--grn-bdr)}
.t-new{background:var(--pur-bg);color:var(--pur);border-color:var(--pur-bdr);font-size:10px}
.qchev{flex-shrink:0;color:var(--tx3);padding-top:3px;transition:transform .2s;font-size:14px}
.qb{padding:0 16px 18px;border-top:1px solid var(--bdr)}
.blbl{font-size:10px;font-weight:600;letter-spacing:.08em;text-transform:uppercase;color:var(--tx3);margin:14px 0 8px;display:flex;align-items:center;gap:6px}
.blbl::after{content:'';flex:1;height:1px;background:var(--bdr)}
.abx{background:var(--surf2);border:1px solid var(--bdr);border-left:3px solid var(--acc);border-radius:var(--r);padding:12px 14px;font-size:13px;line-height:1.75}
.tbx{background:var(--surf2);border:1px solid var(--bdr);border-left:3px solid var(--grn);border-radius:var(--r);padding:12px 14px;font-size:13px;line-height:1.75}
.fql{display:flex;flex-direction:column}
.fqi{border-bottom:1px solid var(--bdr)}.fqi:last-child{border-bottom:none}
.fqh{display:flex;gap:8px;align-items:flex-start;padding:8px 0;cursor:pointer;user-select:none}
.fqh:hover .fqq{color:var(--acc)}
.fqa-arr{color:var(--tx3);flex-shrink:0;font-size:11px;padding-top:2px}
.fqq{font-size:13px;color:var(--tx2);flex:1;transition:color .12s}
.fqchev{color:var(--tx3);flex-shrink:0;font-size:12px;transition:transform .15s;padding-top:3px}
.fqans{padding:8px 12px 10px 24px;font-size:13px;color:var(--tx2);line-height:1.75;background:var(--surf3);border-radius:var(--r);margin:2px 0 6px 18px;border-left:2px solid var(--acc)}
.diag-grid{display:flex;flex-direction:column;gap:12px;margin-top:8px}
.diag-card{background:var(--surf2);border:1px solid var(--bdr);border-radius:var(--r);overflow:hidden;cursor:pointer;transition:border-color .15s,box-shadow .15s}
.diag-card:hover{border-color:var(--acc-bdr);box-shadow:0 0 0 1px var(--acc-bg)}
.diag-title{font-size:11px;color:var(--tx2);padding:7px 10px;border-bottom:1px solid var(--bdr);font-weight:500;display:flex;align-items:center;justify-content:space-between;gap:8px}
.diag-hint{font-size:10px;color:var(--tx3);font-weight:400;flex-shrink:0}
.diag-svg{padding:6px 8px 10px}
.diag-card svg{width:100%;height:auto;display:block;min-height:140px}
.diag-lb{display:none;position:fixed;inset:0;z-index:200;align-items:center;justify-content:center;padding:16px}
.diag-lb.open{display:flex}
.diag-lb-backdrop{position:absolute;inset:0;background:rgba(1,4,9,.82);backdrop-filter:blur(4px)}
.diag-lb-panel{position:relative;z-index:1;width:min(96vw,1100px);max-height:92vh;background:var(--surf);border:1px solid var(--bdr);border-radius:var(--rl);display:flex;flex-direction:column;overflow:hidden;box-shadow:0 24px 80px rgba(0,0,0,.55)}
.diag-lb-hdr{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:12px 16px;border-bottom:1px solid var(--bdr)}
.diag-lb-title{font-size:13px;font-weight:500;color:var(--tx)}
.diag-lb-close{background:var(--surf3);border:1px solid var(--bdr);border-radius:var(--r);color:var(--tx2);width:32px;height:32px;cursor:pointer;font-size:14px;line-height:1}
.diag-lb-close:hover{color:var(--tx);border-color:var(--bdr2)}
.diag-lb-toolbar{display:flex;align-items:center;gap:8px;padding:8px 16px;border-bottom:1px solid var(--bdr);flex-wrap:wrap}
.diag-lb-hint{font-size:11px;color:var(--tx3);flex:1;min-width:140px}
.zbtn{background:var(--surf3);border:1px solid var(--bdr);border-radius:6px;color:var(--tx2);font-size:12px;padding:5px 10px;cursor:pointer;font-family:var(--sans)}
.zbtn:hover{color:var(--tx);border-color:var(--bdr2)}
.diag-lb-viewport{flex:1;min-height:280px;max-height:calc(92vh - 120px);overflow:hidden;cursor:grab;background:var(--surf2);touch-action:none}
.diag-lb-viewport.dragging{cursor:grabbing}
.diag-lb-inner{transform-origin:center center;display:flex;align-items:center;justify-content:center;min-height:100%;padding:20px;will-change:transform}
.diag-lb-inner svg{width:100%;max-width:min(100%,980px);height:auto;display:block}
.bact{display:flex;gap:8px;margin-top:14px;padding-top:14px;border-top:1px solid var(--bdr)}
.abtn{display:inline-flex;align-items:center;gap:6px;font-size:12px;font-family:var(--sans);padding:6px 12px;border-radius:var(--r);cursor:pointer;border:1px solid;transition:all .12s}
.abtn-g{background:var(--grn-bg);color:var(--grn);border-color:var(--grn-bdr)}.abtn-g:hover{background:rgba(63,185,80,.18)}
.abtn-n{background:var(--surf3);color:var(--tx2);border-color:var(--bdr)}.abtn-n:hover{color:var(--tx);border-color:var(--bdr2)}
.empty{text-align:center;padding:60px 20px;color:var(--tx2)}
.empty-n{font-size:32px;margin-bottom:12px;color:var(--tx3)}.empty-t{font-size:15px;font-weight:500;color:var(--tx);margin-bottom:6px}
::-webkit-scrollbar{width:6px}::-webkit-scrollbar-track{background:transparent}::-webkit-scrollbar-thumb{background:var(--bdr);border-radius:3px}
@media(max-width:700px){.sb{display:none}.stats{grid-template-columns:repeat(2,1fr)}.hdr-r{display:none}}
@media(prefers-reduced-motion:reduce){*{transition:none!important}}
</style>
</head>
<body>
<header class="hdr">
<span class="logo">EDA</span>
<div><div class="hdr-t">Event-Driven Architecture — Interview Question Bank</div><div class="hdr-s">26 questions · 78 follow-up answers · 12 diagrams</div></div>
<div class="hdr-r">
<a class="hdr-link" href="index.html">Index</a>
<a class="hdr-link" href="system_design_cheatsheet_v14.html">v15</a>
<div class="pw"><span class="pl">Reviewed</span><div class="pb"><div class="pf" id="pf"></div></div><span class="pc" id="pc">0 / 26</span></div>
</div>
</header>
<div class="layout">
<aside class="sb" id="sb"></aside>
<main class="main">
<div class="stats">
<div class="stat"><div class="stn" id="s-show">26</div><div class="stl">Showing</div></div>
<div class="stat"><div class="stn">10</div><div class="stl">Categories</div></div>
<div class="stat"><div class="stn" id="s-rev">0</div><div class="stl">Reviewed</div></div>
<div class="stat"><div class="stn" id="s-hard">20</div><div class="stl">Hard questions</div></div>
</div>
<div class="ctrls">
<div class="sw">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/></svg>
<input class="si" type="text" id="srch" placeholder="Search questions…" autocomplete="off">
</div>
<button class="cb" id="btn-hr" onclick="thr()">Hide reviewed</button>
<button class="cb" id="btn-ho" onclick="tho()">Hard only</button>
<button class="cb" onclick="ea()">Expand all</button>
<button class="cb" onclick="ca()">Collapse all</button>
</div>
<div class="ql" id="ql"></div>
</main>
</div>
<div id="diag-lb" class="diag-lb" aria-hidden="true" role="dialog" aria-modal="true" aria-labelledby="diag-lb-title">
<div class="diag-lb-backdrop"></div>
<div class="diag-lb-panel">
<div class="diag-lb-hdr">
<span id="diag-lb-title" class="diag-lb-title"></span>
<button type="button" class="diag-lb-close" id="diag-lb-close" aria-label="Close diagram">✕</button>
</div>
<div class="diag-lb-toolbar">
<span class="diag-lb-hint">Scroll to zoom · drag to pan · double-click to reset · Esc to close</span>
<button type="button" class="zbtn" data-lb-zoom="-">−</button>
<button type="button" class="zbtn" data-lb-zoom="0">Fit</button>
<button type="button" class="zbtn" data-lb-zoom="+">+</button>
</div>
<div class="diag-lb-viewport" id="diag-lb-vp">
<div class="diag-lb-inner" id="diag-lb-inner"></div>
</div>
</div>
</div>
<script>
var CM={"delivery": {"label": "Delivery semantics", "dot": "#2F81F7", "isNew": false}, "ordering": {"label": "Ordering", "dot": "#D29922", "isNew": false}, "failure": {"label": "Failure handling", "dot": "#F85149", "isNew": false}, "kafka": {"label": "Kafka internals", "dot": "#3FB950", "isNew": false}, "patterns": {"label": "Patterns", "dot": "#8B949E", "isNew": false}, "backpressure": {"label": "Backpressure", "dot": "#E3B341", "isNew": true}, "schema": {"label": "Schema evolution", "dot": "#BC8CFF", "isNew": true}, "observability": {"label": "Observability", "dot": "#39D353", "isNew": true}, "async": {"label": "Async vs sync", "dot": "#FF7B72", "isNew": true}, "realworld": {"label": "Real-world failures", "dot": "#F85149", "isNew": true}};
var QS=[{"id": 1, "cat": "delivery", "diff": "med", "title": "What is the difference between at-most-once, at-least-once, and exactly-once delivery?", "answer": "At-most-once: commit the offset before processing. If the consumer crashes after commit but before processing, the message is lost. Used when loss is acceptable \u2014 metrics, analytics. At-least-once: commit after processing. If the consumer crashes after processing but before commit, the message reprocesses on restart. Duplicates possible \u2014 requires idempotent consumer. Exactly-once: idempotent producer (assigns sequence numbers, broker deduplicates) plus transactional consumer (atomic write and offset commit in a Kafka transaction). Significantly lower throughput than at-least-once due to 2-phase commit overhead.", "tradeoff": "At-least-once plus idempotent consumer is the production default for 99% of systems. Exactly-once is justified only for financial transactions or cases where deduplication at the consumer is impossible or expensive.", "fq": ["What makes a consumer idempotent?", "When would you accept at-most-once delivery?", "How does Kafka implement exactly-once with transactions?"], "fqa": ["A consumer is idempotent when processing the same message twice produces the same result as processing it once. Techniques: natural idempotency (SET operations instead of INCREMENT), deduplication via unique message ID stored in a Redis set or DB unique constraint (if message_id already exists, ack and skip), and versioned updates (only apply if incoming version > stored version).", "Accept at-most-once when the cost of occasional loss is lower than the cost of deduplication logic. Examples: metrics aggregation (losing one data point does not affect the aggregate meaningfully), real-time dashboards (stale data is worse than missing data), and telemetry pipelines where volume is so high that occasional loss is statistically invisible.", "Kafka exactly-once requires three coordinated features: (1) enable.idempotence=true on the producer \u2014 the broker assigns a PID and deduplicates by PID+sequence, (2) transactional.id set on the producer \u2014 enables the transaction coordinator to manage atomic multi-partition commits, (3) isolation.level=read_committed on the consumer \u2014 messages from aborted transactions are invisible. The transaction lifecycle: beginTransaction \u2192 send \u2192 sendOffsetsToTransaction \u2192 commitTransaction. On producer crash, the coordinator aborts incomplete transactions on restart."]}, {"id": 2, "cat": "delivery", "diff": "hard", "title": "How does Kafka implement exactly-once semantics end-to-end?", "answer": "Three components. Idempotent producer: each producer gets a PID, each message a monotonic sequence number per partition \u2014 the broker rejects duplicates with the same PID and sequence. Transactional producer: wraps messages across partitions in a transaction \u2014 either all commit or none do. Transactional consumer: isolation.level=read_committed means the consumer only sees messages from committed transactions. Combined, producer retries do not duplicate, multi-partition writes are atomic, and consumers never see partial writes.", "tradeoff": "Throughput drops 20-30% vs. at-least-once because of coordinator round-trips for transaction commit. For most systems, at-least-once with a Redis dedup store is cheaper and simpler.", "fq": ["What is the role of the transaction coordinator?", "What happens if the producer crashes mid-transaction?", "How does read_committed isolation affect consumer lag?"], "fqa": ["The transaction coordinator is a Kafka broker elected to manage the lifecycle of a specific producer's transactions. It maintains a transaction log recording state: ONGOING, PREPARE_COMMIT, COMPLETE_COMMIT, or ABORT. The coordinator ensures that if a producer crashes, the transaction is aborted on recovery \u2014 preventing partial writes from becoming visible. Each transactional.id maps deterministically to one coordinator broker via hashing.", "If the producer crashes mid-transaction, the coordinator detects the producer has gone silent (transaction.timeout.ms expires) and aborts the transaction. All messages written during the aborted transaction are marked with an abort marker. Consumers with isolation.level=read_committed skip these messages. On producer restart with the same transactional.id, it calls initTransactions() which fences any zombie instances of the same producer ID.", "read_committed consumers cannot read past the Last Stable Offset (LSO) \u2014 the offset of the oldest open transaction. If a long-running transaction is open, the LSO does not advance, and consumer lag grows even though messages are being written. This means a slow or stuck producer transaction directly increases consumer lag for all read_committed consumers on the same partition. Monitor LSO lag separately from consumer group lag to detect this."]}, {"id": 3, "cat": "ordering", "diff": "hard", "title": "SQS FIFO guarantees ordering \u2014 why can messages still arrive out of order in practice?", "answer": "FIFO preserves order as received \u2014 it does not create order. Three real-world breaks: first, the producer sends out of order (a retry causes UPDATE v3 before v2). Second, multiple producers on the same message group key race without coordination. Third, failed messages get requeued after their visibility timeout \u2014 they arrive after messages published later, breaking logical order.", "tradeoff": "The real solution is version numbers embedded in the event at publish time, not relying on queue delivery order. FIFO reduces reordering probability but does not eliminate the need for version-based consumer logic.", "fq": ["How do you handle a gap where the missing version never arrives?", "When would you choose Kafka over SQS FIFO for ordering?", "What is the maximum throughput of SQS FIFO?"], "fqa": ["Gap handling requires a timeout-based fallback. Buffer the out-of-order event in memory or a Redis sorted set keyed by entity ID, scored by version. Wait up to a configurable gap timeout (e.g. 5 minutes). If the missing version arrives within the window, apply both in order. If the timeout fires without the missing version, trigger reconciliation: read the authoritative current state from the source-of-truth database and overwrite the derived state. Never leave a gap unresolved \u2014 silent inconsistency is worse than a reconciliation overhead.", "Choose Kafka over SQS FIFO when: (1) you need multiple independent consumer groups reading the same stream \u2014 SQS FIFO delivers to one consumer group only, (2) throughput exceeds ~3,000 messages/sec per message group \u2014 SQS FIFO hard limit is 3,000 TPS per group, (3) you need event replay \u2014 SQS deletes messages on consume, (4) you need stream processing with Kafka Streams or Flink, (5) ordering must be guaranteed across more than one consumer type simultaneously.", "SQS FIFO throughput limits: 300 API calls/sec per queue without batching, 3,000 messages/sec per queue with batching (10 messages per batch). Per message group, ordering is strict but throughput is shared across the queue. Standard SQS has no ordering guarantee but no throughput limit. For workloads requiring both strict ordering and throughput above 3K/sec, Kafka with a single partition per entity is the correct choice."]}, {"id": 4, "cat": "ordering", "diff": "hard", "title": "How does Kafka guarantee ordering and what breaks it?", "answer": "Kafka guarantees ordering within a partition. All messages with the same partition key go to the same partition \u2014 a strictly ordered append-only log. What breaks it: a producer sends to multiple partitions; consumer rebalance temporarily causes ordering issues; acks=1 with a leader failure loses messages.", "tradeoff": "acks=all with min.insync.replicas=2 eliminates the leader-failure ordering break at the cost of one extra replica round-trip per write.", "fq": ["How does a consumer rebalance affect offset commits?", "What is the ISR and why does it matter for ordering?", "When does acks=1 lose messages?"], "fqa": ["During a rebalance, all consumers in the group pause consumption \u2014 a stop-the-world event. Any in-flight messages that were fetched but not yet committed have their offsets reset to the last committed position. The new consumer assigned that partition starts from the last committed offset, reprocessing messages the previous consumer already handled. Mitigation: commit offsets synchronously before the rebalance using a ConsumerRebalanceListener.onPartitionsRevoked() callback \u2014 this gives you a hook to flush and commit before partitions are reassigned.", "The ISR (In-Sync Replica set) is the set of partition replicas that are fully caught up to the leader within replica.lag.time.max.ms. Only ISR members are eligible for leader election on broker failure. With min.insync.replicas=2, a write is only acknowledged when at least 2 ISR replicas have persisted it \u2014 so even if the leader fails immediately after, the elected follower has the data. Without ISR enforcement (acks=1), a message acknowledged by only the leader can be lost if that leader fails before replication.", "acks=1 loses messages in this specific sequence: (1) producer sends message, (2) leader writes to its log and sends ack, (3) leader fails before any follower has replicated the message, (4) a follower (which does not have the message) is elected as the new leader, (5) from the new leader's perspective, that message never existed. The producer received an ack but the message is permanently gone. This is called a log divergence. acks=all prevents this."]}, {"id": 5, "cat": "failure", "diff": "hard", "title": "A downstream service is intermittently failing \u2014 30% of messages fail. How do you prevent data loss?", "answer": "Three layers: never ack until processing succeeds, exponential backoff with jitter, circuit breaker to stop polling, and DLQ after max retries with controlled replay.", "tradeoff": "Set visibility timeout to 6x expected processing time. Set max receive count to give at least 30 minutes of retry window before DLQ routing.", "fq": ["How do you tune visibility timeout for a slow consumer?", "What is the difference between a DLQ and a retry queue?", "How do you replay from the DLQ without re-triggering the outage?"], "fqa": ["Visibility timeout tuning: measure the p99 processing time for your consumer under normal load. Set visibility timeout = 6 x p99. This gives the consumer enough time to finish even under degraded conditions, while ensuring that a truly stuck or crashed consumer eventually times out and the message becomes visible again. If processing is highly variable, implement heartbeating: periodically call ChangeMessageVisibility to extend the timeout while processing is still in progress.", "A DLQ (Dead Letter Queue) is the final destination for messages that have exhausted all retry attempts \u2014 they go there to be inspected and replayed manually after the bug is fixed. A retry queue is an intermediate queue that implements a delayed retry strategy: messages are sent to the retry queue with a delay attribute, processed after the delay expires, and if they still fail, promoted to the next retry tier or eventually to the DLQ. The retry queue pattern gives finer control over retry delays (exponential tiers: 30s, 5min, 30min, then DLQ).", "Safe DLQ replay requires three conditions: (1) confirm the root cause is fixed and the downstream service is healthy, (2) replay in a rate-limited, controlled way \u2014 do not drain the full DLQ at once, as this can re-trigger the original overload, (3) replay in a canary manner first \u2014 send 1% of DLQ messages and verify they succeed before replaying the rest. AWS SQS DLQ redrive policy automates replay back to the source queue."]}, {"id": 6, "cat": "failure", "diff": "hard", "title": "What is the outbox pattern and why is it necessary?", "answer": "Solves the dual-write problem by writing to an outbox table in the same ACID transaction as the business record. An async worker polls and publishes with retries.", "tradeoff": "The outbox worker delivers at-least-once. CDC via Debezium reads the DB transaction log directly \u2014 no application changes needed, but requires monitoring replication slot lag.", "fq": ["How do you prevent the outbox table from growing unbounded?", "What is CDC as an alternative to the outbox?", "How does the outbox pattern interact with database sharding?"], "fqa": ["Outbox table growth is controlled by the worker: after successfully publishing a message, the worker updates the row status to PROCESSED. A separate cleanup job (scheduled daily or hourly) deletes rows older than your audit retention period (e.g. 7 days) where status=PROCESSED. Add a composite index on (status, created_at) so the cleanup query is efficient. Never delete on publish \u2014 keep rows for the audit retention window in case you need to replay.", "CDC (Change Data Capture) via Debezium reads the database transaction log (WAL for PostgreSQL, binlog for MySQL) and publishes every row-level change to Kafka as an event. No application code changes needed \u2014 the event is emitted by the database engine itself. Advantages over outbox: no polling delay, no outbox table to maintain, captures changes from any writer. Disadvantages: requires setting up a replication slot which prevents WAL cleanup if Debezium falls behind \u2014 monitor replication slot lag as a P1 metric.", "With database sharding, the outbox table lives on the same shard as the business record \u2014 the ACID transaction boundary is per-shard. The outbox worker must poll each shard independently. This means N shard workers running in parallel, each consuming from one shard's outbox. For global ordering across shards, the workers must publish to Kafka with a partition key that preserves per-entity ordering, accepting that cross-entity global ordering is not guaranteed."]}, {"id": 7, "cat": "failure", "diff": "med", "title": "What happens when a Kafka consumer group rebalances and how do you handle it safely?", "answer": "All consumers pause during rebalance. Uncommitted offsets are reset. Commit only after full batch processing. Use pause() to avoid false rebalances.", "tradeoff": "Practical default: commit every 5 seconds or every 500 messages, whichever comes first.", "fq": ["What is a cooperative rebalance vs. eager rebalance?", "How does max.poll.interval.ms affect rebalance behavior?", "What is the difference between auto-commit and manual commit?"], "fqa": ["Eager rebalance (default before Kafka 2.4): ALL partitions are revoked from ALL consumers before reassignment begins. Every consumer stops processing, partitions are redistributed, and then consumers resume. Causes a full stop-the-world pause. Cooperative rebalance (incremental, default from Kafka 3.1): only the partitions that need to move are revoked. Consumers that keep their partitions continue processing throughout the rebalance. Enable with partition.assignment.strategy=CooperativeStickyAssignor.", "max.poll.interval.ms is the maximum time between two consecutive poll() calls before the broker considers the consumer dead and triggers a rebalance. Default is 5 minutes. If your processing logic takes longer than this between polls, the broker kicks the consumer out of the group \u2014 even though it is alive and processing. Fix: increase max.poll.interval.ms to match your worst-case processing time, or reduce max.poll.records so each batch finishes faster, or move slow processing to an async thread.", "Auto-commit (enable.auto.commit=true): Kafka commits the offset of the last fetched message on a timer (auto.commit.interval.ms, default 5 seconds). The commit happens regardless of whether processing succeeded \u2014 if the consumer crashes after the auto-commit but before finishing processing, those messages are silently skipped. Manual commit (enable.auto.commit=false): you call consumer.commitSync() or consumer.commitAsync() explicitly after processing each batch. Manual commit gives you exactly-processed semantics at the cost of more code."]}, {"id": 8, "cat": "kafka", "diff": "hard", "title": "How does Kafka achieve high throughput? Walk through the key design decisions.", "answer": "Sequential disk writes, zero-copy transfer via sendfile(), batching, and partitioning for linear horizontal scale.", "tradeoff": "More partitions gives more throughput but more open file handles and longer leader election. Design with headroom but do not over-partition speculatively.", "fq": ["What is the role of the page cache in Kafka performance?", "Why does Kafka not use a database for storage?", "How does compression interact with batching?"], "fqa": ["The page cache is the OS's in-memory buffer of recently read and written disk blocks. Kafka leverages the page cache rather than maintaining its own heap-based cache. When a producer writes to Kafka, the data goes to the page cache first (making writes fast), then asynchronously flushed to disk. When a consumer reads, if the data is still in the page cache, no disk I/O is needed \u2014 the data goes directly from page cache to the network socket via zero-copy. In practice, consumers often read data that was just produced, so the page cache hit rate is very high. Kafka brokers should have as much RAM as possible dedicated to the page cache, not the JVM heap.", "Databases optimize for random access \u2014 they use B-trees, indexes, and buffer pools to efficiently find and update arbitrary rows. This is expensive for a write-heavy sequential log. Kafka's access pattern is purely sequential: producers always append to the end, consumers always read forward from an offset. Sequential I/O on modern SSDs is 10-100x faster than random I/O. By using a simple append-only log file (no indexes, no update-in-place), Kafka avoids all the overhead of database structures. The tradeoff: you cannot efficiently query Kafka by arbitrary fields \u2014 only by offset.", "Compression and batching are synergistic. Compression works better on larger inputs \u2014 similar data (e.g. JSON events with the same field names) compresses at 5-10x ratio when batched together, vs. 2-3x for individual messages. Kafka supports lz4 (fast, moderate), snappy (fast, good), gzip (slower, best), and zstd (best ratio/speed tradeoff). Compression happens at the producer before sending \u2014 the batch is compressed as a unit. The broker stores the compressed batch as-is and the consumer decompresses. CPU overhead at producer and consumer, but network and storage savings are typically worth it."]}, {"id": 9, "cat": "kafka", "diff": "hard", "title": "What is log compaction in Kafka and when do you use it?", "answer": "Retains only the latest message per key. Used for CDC, state rebuilding, and the __consumer_offsets topic. Compaction runs in the background.", "tradeoff": "Compaction lag means the latest write may not yet be compacted. Tombstone messages mark keys for deletion but are retained briefly for consumers to observe.", "fq": ["What is a tombstone record in Kafka?", "How does log compaction interact with consumer offset tracking?", "When would you use compaction and TTL together?"], "fqa": ["A tombstone record is a message with a non-null key and a null value. When the log compactor sees a tombstone, it knows the key should be deleted. The compactor retains the tombstone for a configurable period (delete.retention.ms, default 24 hours) so consumers reading the compacted topic can observe the deletion event. After delete.retention.ms elapses, the tombstone itself is removed. If a consumer is offline for longer than delete.retention.ms, it will miss the deletion event and its local state will contain a stale key \u2014 this is why compacted topic consumers should monitor their lag carefully.", "Log compaction does not affect the __consumer_offsets topic's own compaction \u2014 that topic is always compacted independently by Kafka to retain the latest committed offset per consumer group and partition. For user-defined compacted topics, consumer offset tracking works the same as for regular topics \u2014 offsets are still sequential. The key difference: in a compacted topic, gaps may appear in the offset sequence after compaction (e.g. offsets 100, 105, 203 if intermediate messages were compacted away). Consumers must handle these gaps correctly \u2014 seek to a valid offset, not assume offset continuity.", "Use compaction + TTL together (cleanup.policy=compact,delete) when you need both: retain the latest value per key indefinitely (compaction) AND purge keys that have not been updated for a long time (TTL). Example: a user preferences topic where you want the latest preference per user always available, but want to clean up users who have been inactive for 90 days. The TTL purges old tombstones and outdated records; compaction ensures only the latest value per active key survives."]}, {"id": 10, "cat": "kafka", "diff": "hard", "title": "How does Kafka handle a broker failure without losing messages?", "answer": "ISR-based leader election with acks=all and min.insync.replicas=2 guarantees messages survive a single broker failure.", "tradeoff": "acks=all adds ~1-5ms latency per produce call. For analytics at-most-once workloads, acks=1 is acceptable.", "fq": ["What happens if the ISR shrinks to 1 with min.insync.replicas=2?", "What is unclean leader election and why is it dangerous?", "How does KRaft replace ZooKeeper for leader election?"], "fqa": ["If the ISR shrinks to 1 (only the leader remains in sync) and min.insync.replicas=2, the broker rejects all produce requests with NotEnoughReplicasException. No new messages can be written until at least one follower catches up and rejoins the ISR. This is the correct behavior \u2014 it refuses to acknowledge writes that cannot meet the durability guarantee. To recover: investigate why the follower fell behind, resolve the root cause, and wait for the follower to catch up. You can temporarily lower min.insync.replicas to 1 to restore availability, accepting reduced durability.", "Unclean leader election allows an out-of-sync replica (one not in the ISR) to be elected as leader when no ISR member is available. This restores availability at the cost of data loss \u2014 the out-of-sync replica becomes the new leader but is missing messages that were acknowledged by the previous leader. Enable with unclean.leader.election.enable=true (default false in Kafka 3.x). Only consider enabling for topics where availability is more important than durability \u2014 analytics pipelines. Never enable for financial or audit topics.", "ZooKeeper was used by Kafka for cluster metadata: broker registration, topic/partition configuration, leader election, and consumer group coordination. KRaft (Kafka Raft Metadata) replaces ZooKeeper by moving all metadata into Kafka itself \u2014 a special metadata log replicated via the Raft consensus protocol among controller nodes. Benefits: fewer components to operate, faster leader election (milliseconds vs. seconds), ability to scale to millions of partitions. KRaft became production-ready in Kafka 3.3 and ZooKeeper mode is deprecated."]}, {"id": 11, "cat": "patterns", "diff": "hard", "title": "What is the saga pattern and when do you use it instead of a distributed transaction?", "answer": "Sequence of local transactions with compensating transactions on failure. Used when 2PC is not feasible across microservices.", "tradeoff": "Sagas are eventually consistent. Compensating transactions must be idempotent and always succeed. Reserve, do not debit, in step 1.", "fq": ["What is a compensating transaction and what makes one valid?", "How does the saga orchestrator handle its own failure mid-saga?", "Choreography vs. orchestration \u2014 when do you choose each?"], "fqa": ["A compensating transaction is a business operation that undoes the effect of a previous saga step. Three requirements for validity: (1) It must always succeed \u2014 a compensating transaction that can fail leaves the saga in an unresolvable stuck state. This is why saga steps should use reversible operations (reserve funds, not debit funds). (2) It must be idempotent \u2014 if the compensation is retried, applying it twice must produce the same result as once. (3) It must be semantically correct \u2014 undoing step N should restore the exact pre-step-N state. Some operations cannot be compensated (sending an email) \u2014 for these, design the saga so these steps happen last.", "The saga orchestrator is itself a stateful service that can fail. Recovery strategy: the orchestrator persists saga state (current step, each step's status, correlation ID) to a durable store before executing each step. On restart, it reads the persisted state and resumes from the last known step. If a step was in-flight when the orchestrator crashed, it retries it \u2014 this is why all saga participants must be idempotent. If the orchestrator is unavailable for a prolonged period, saga instances time out and are marked for manual intervention.", "Choreography: each service publishes an event when its step completes; the next service subscribes and reacts. Advantages: loose coupling, no single point of failure, easy to add new steps. Disadvantages: hard to visualize the full saga flow, difficult to debug failures, hard to implement rollback across many services. Orchestration: a central saga orchestrator sends commands to each service and waits for responses. Advantages: full saga flow is visible in one place, easier to implement timeouts and rollbacks. Disadvantages: orchestrator is a single point of failure. Choose choreography for simple sagas with 2-3 steps; orchestration for complex sagas with branching and rollback logic."]}, {"id": 12, "cat": "patterns", "diff": "med", "title": "What is consumer lag and how do you detect and respond to it?", "answer": "Consumer lag = latest produced offset minus last committed offset. Alert on growing lag, not absolute value. Scale consumers up to partition count.", "tradeoff": "Partition count is the ceiling for consumer parallelism. Extra consumers beyond partition count sit idle.", "fq": ["What is the maximum useful number of consumers in a group?", "How does consumer lag interact with offset retention?", "What causes a consumer to lag without increasing CPU?"], "fqa": ["The maximum useful number of consumers in a consumer group equals the number of partitions in the topic. With 12 partitions, at most 12 consumers can be active simultaneously \u2014 each consumer gets at least one partition. A 13th consumer would sit idle. Design partitions for your target parallelism: if you anticipate needing 20 parallel consumers at peak, create at least 20 partitions at topic creation time. Increasing partitions later requires recreating the topic in older Kafka versions.", "Kafka retains committed offsets in the __consumer_offsets internal topic, which has its own retention policy (offsets.retention.minutes, default 7 days). If a consumer group is inactive for longer than this period, its committed offsets are deleted. When the group resumes consuming, it has no committed offset \u2014 the consumer falls back to auto.offset.reset (earliest or latest). With auto.offset.reset=earliest, the consumer reprocesses all messages from the beginning. This is why monitoring consumer group health even for infrequently running batch consumers is important.", "A consumer can lag without high CPU in several scenarios: (1) the consumer is doing network-bound work \u2014 calling a downstream API that is slow, so threads are waiting rather than consuming CPU, (2) the consumer is doing disk-bound work \u2014 writing to a database with high latency, (3) the consumer is being throttled by a downstream service, (4) the consumer pool is too small \u2014 fewer threads than available CPU, (5) the consumer is spending time in GC pauses \u2014 check GC logs, (6) the consumer has lock contention \u2014 threads are blocking on each other."]}, {"id": 13, "cat": "patterns", "diff": "hard", "title": "What is CDC and how does Debezium implement it?", "answer": "CDC reads the database transaction log and publishes row-level changes as events to Kafka. Debezium tails the WAL/binlog with zero application impact.", "tradeoff": "PostgreSQL replication slot prevents WAL cleanup if Debezium falls behind. Monitor replication slot lag as a P1 metric.", "fq": ["How does a Debezium replication slot affect PostgreSQL performance?", "What is the schema registry and why does CDC need it?", "How do you handle CDC for a table with no primary key?"], "fqa": ["A PostgreSQL logical replication slot tells PostgreSQL to retain WAL segments until the slot consumer (Debezium) has read them. If Debezium falls behind or goes offline, WAL segments accumulate indefinitely. The impact: disk fills up, PostgreSQL cannot reclaim WAL space. Set max_slot_wal_keep_size (PostgreSQL 13+) which causes PostgreSQL to invalidate the slot if WAL retention would exceed the limit \u2014 better to invalidate than to run out of disk. Monitor slot lag with: SELECT pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn) FROM pg_replication_slots.", "The Confluent Schema Registry is a REST service that stores versioned schemas for Kafka topics. CDC needs it because the schema of a database table can evolve \u2014 columns are added, renamed, or removed. When Debezium publishes a change event, it uses the table's current schema. The consumer looks up the schema by ID from the registry before deserializing. This enables schema evolution without breaking consumers \u2014 as long as the evolution is backward compatible.", "Without a primary key, Debezium cannot reliably identify which row changed in an UPDATE or DELETE operation. Options: (1) add a surrogate primary key \u2014 ALTER TABLE ADD COLUMN id SERIAL PRIMARY KEY (best option), (2) use REPLICA IDENTITY FULL on the table \u2014 PostgreSQL logs the full before-image of every row on UPDATE/DELETE, allowing Debezium to match by comparing the full row, at the cost of much larger WAL entries, (3) for append-only tables (INSERT only, no UPDATE or DELETE), no primary key is needed."]}, {"id": 14, "cat": "patterns", "diff": "hard", "title": "Kafka vs. SQS \u2014 how do you choose?", "answer": "Kafka: replay, multiple consumer groups, ordering, stream processing, high throughput. SQS: simple task queue, zero ops, point-to-point, no replay needed.", "tradeoff": "SQS messages are deleted on consume \u2014 no replay. This single constraint eliminates SQS for any use case requiring event history.", "fq": ["Can you use SNS to fan-out to multiple SQS queues as a Kafka substitute?", "What is Amazon MSK and when would you use it over self-managed Kafka?", "How does Kinesis compare to both Kafka and SQS?"], "fqa": ["SNS fan-out to multiple SQS queues is a valid pattern for simple pub/sub: one SNS topic, multiple SQS subscriptions, each subscription delivers a copy of each message to a different SQS queue. Limitations vs. Kafka: (1) no replay \u2014 each SQS queue still deletes messages on consume, (2) ordering is not guaranteed in Standard SQS, (3) all subscribers receive all messages \u2014 you cannot have one subscriber read from offset 0 and another from offset 1000 independently, (4) operational cost scales with number of queues. Use SNS+SQS for simple fan-out to a small number of consumers with no replay requirement.", "Amazon MSK (Managed Streaming for Apache Kafka) is a fully managed Kafka service on AWS. Use MSK over self-managed Kafka when: you want to eliminate Kafka operational overhead (broker management, upgrades, ZooKeeper ops), you are already AWS-native and want IAM-based authentication, you need predictable cost. Use self-managed Kafka when: you need the latest Kafka version faster than MSK supports it, you have specific broker configuration requirements MSK does not expose, you are multi-cloud and need portability.", "Kinesis is AWS's managed event streaming service, positioned between SQS and Kafka. Like Kafka: messages are retained for up to 365 days, multiple consumers can independently read the same stream, shards provide ordered partitioning. Unlike Kafka: native AWS integration with Lambda and Firehose, vendor lock-in, maximum retention 365 days (Kafka is configurable indefinitely), consumer throughput limited per shard. Choose Kinesis when you are AWS-native and want zero operational overhead with replay capability. Choose Kafka when you need the full Kafka ecosystem or cross-cloud portability."]}, {"id": 15, "cat": "backpressure", "diff": "hard", "title": "What is backpressure and how do you implement it in an event-driven system?", "answer": "Backpressure signals upstream to slow down. Implement via bounded queues, circuit breakers, producer rate limiting, or reactive pull-based model.", "tradeoff": "Blocking backpressure ties up producer threads. Reactive backpressure requires the full pipeline to be reactive.", "fq": ["What is the difference between backpressure and rate limiting?", "How does Project Reactor implement backpressure with Flux?", "What happens when the bounded queue is full \u2014 block, drop, or shed load?"], "fqa": ["Rate limiting controls the maximum rate at which a producer emits messages, regardless of consumer speed \u2014 it is a policy enforced at the producer. Backpressure is a signal from the consumer to the producer to slow down \u2014 it is demand-driven and dynamic. Rate limiting is static (100 req/sec always); backpressure is adaptive (slow down now because I am overloaded, speed up when I recover). You typically need both: rate limiting as a ceiling, backpressure as a dynamic adjustment.", "Project Reactor implements backpressure through the reactive streams specification. A Flux is a publisher of 0..N items. A subscriber signals demand by calling request(n) on the subscription \u2014 it says 'I am ready to receive n more items.' The publisher only emits up to n items before waiting for the next request(n) signal. Operators like onBackpressureBuffer() buffer items when downstream is slower. onBackpressureDrop() drops items the downstream cannot consume. onBackpressureLatest() keeps only the most recent item. For Kafka integration, reactor-kafka provides reactive Kafka consumers that propagate backpressure from processing logic back to the poll loop.", "When the bounded queue is full, you have three options: (1) Block \u2014 the producer thread calls BlockingQueue.put() and waits until space is available. Simple, preserves all work, but ties up the producer thread and can cascade upstream. Best when the queue is an internal buffer and the producer thread blocking is acceptable. (2) Drop and log \u2014 reject the item with a logged warning. Best for metrics, telemetry, or real-time data where freshness matters more than completeness. (3) Shed load with 429/503 \u2014 for inbound HTTP requests, return HTTP 429/503 immediately rather than queueing. The client retries with backoff."]}, {"id": 16, "cat": "backpressure", "diff": "med", "title": "What is the difference between load shedding and backpressure? When do you choose each?", "answer": "Backpressure slows the producer. Load shedding drops work. Use backpressure when loss is unacceptable; use load shedding when staleness makes work worthless.", "tradeoff": "Backpressure can cascade and stall the pipeline. Load shedding avoids cascade at the cost of dropped work.", "fq": ["How do you implement priority-based load shedding in SQS?", "What is the leaky bucket algorithm and how does it enforce load shedding?", "How does Nginx implement load shedding with connection limits?"], "fqa": ["Priority-based load shedding in SQS: SQS does not natively support message priority. Implement it with multiple queues: one high-priority queue and one low-priority queue. The consumer always polls the high-priority queue first; if it is empty, it polls the low-priority queue. Under load, messages in the low-priority queue accumulate (effectively being shed) while high-priority messages continue to be processed. For more granular priority, use N queues with a weighted polling strategy: poll the high queue 10 times for every 1 time you poll the low queue.", "The leaky bucket algorithm enforces a strict constant output rate regardless of input burst. Imagine a bucket with a hole at the bottom: water (requests) pours in at any rate, but drains out at a fixed rate. If the bucket overflows, excess requests are dropped. This enforces a smooth, steady output rate \u2014 no bursting allowed. Contrast with token bucket: token bucket allows bursting up to the bucket capacity (tokens accumulate during idle periods). Leaky bucket is better for enforcing a strict SLA on a downstream service that cannot handle bursts.", "Nginx load shedding uses two directives: limit_conn (limits simultaneous connections per key) and limit_req (limits request rate per key using a leaky bucket). When limits are exceeded, Nginx returns 503 immediately (if burst is not configured) or queues a limited number of excess requests (if burst=N is set). The limit_req_zone directive defines the shared memory zone for tracking request counts. Example: limit_req_zone $binary_remote_addr zone=api:10m rate=100r/s limits each IP to 100 requests/second with the zone stored in 10MB of shared memory."]}, {"id": 17, "cat": "backpressure", "diff": "hard", "title": "Consumer calling external API rate-limited to 100 req/sec with bursts of 500+. Walk through your full design.", "answer": "Sliding window rate limiter in Redis via Lua script, bounded thread pool for concurrency, exponential backoff with jitter on 429.", "tradeoff": "Token bucket for bursty-but-infrequent clients. Sliding window to mirror external API enforcement model. Schedule instead of busy-wait for CPU efficiency.", "fq": ["How does the Redis Lua script eliminate the race condition in distributed rate limiting?", "Token bucket vs. sliding window \u2014 when do you choose each?", "What happens to your rate limiter if Redis goes down?"], "fqa": ["Without Lua, a distributed rate limiter requires multiple round-trips: GET the current count, check if under limit, INCR if so. Between the GET and INCR, another instance can read the same count and also decide to proceed \u2014 a classic check-then-act race condition that allows bursts above the limit. The Lua script runs atomically on the Redis server: the entire check-and-increment logic executes as a single operation with no interleaving. Redis is single-threaded for command processing, so the Lua script cannot be interrupted. The script also sets the expiry in the same atomic operation, preventing a missing TTL that would cause the key to persist indefinitely.", "Token bucket: tokens accumulate during idle periods (up to bucket capacity). A burst of requests spends accumulated tokens instantly. Best for clients that are inactive for long periods then burst \u2014 they should not be penalized for previous inactivity. Sliding window: counts all requests in the last N seconds regardless of when they happened. No burst credit accumulates. Best for mirroring how the external API itself enforces limits \u2014 if the external API uses a sliding window (most do), a sliding window rate limiter in your client matches its enforcement exactly, preventing accidental 429s due to burst credit mismatch.", "When Redis goes down, the rate limiter has no state \u2014 you must choose between fail-open and fail-closed. Fail-open: allow all requests through when Redis is unavailable. Pros: no availability impact. Cons: the downstream external API gets hammered. Fail-closed: reject all requests or fall back to a very conservative local rate limit when Redis is unavailable. Pros: protects the external API. Cons: your service is degraded. The right choice depends on the external API's consequences for overage. For strict rate limits (payment API), fail-closed. For soft limits, fail-open. Always circuit break Redis itself \u2014 detect unavailability fast and switch to the fallback immediately."]}, {"id": 18, "cat": "schema", "diff": "hard", "title": "What is schema evolution and what makes a change backward or forward compatible?", "answer": "Backward compatible: new consumer reads old messages. Forward compatible: old consumer reads new messages. Fully compatible: add optional fields only.", "tradeoff": "Avro/Protobuf enforce compatibility at registration time. JSON discovers incompatibility at runtime in production.", "fq": ["What is the Confluent Schema Registry and how does it work?", "Why does Avro not include field names in the serialized payload?", "How do you migrate a topic to a new schema when backward compatibility is broken?"], "fqa": ["The Confluent Schema Registry is a REST service that stores versioned schemas for Kafka topics. Each topic has a subject (topic-key or topic-value), and each subject can have multiple schema versions. When a producer serializes a message, the Avro/Protobuf serializer checks the registry: if the schema is new, it registers it and gets a schema ID; if it already exists, it gets the existing ID. The serializer writes the schema ID (4 bytes) as a prefix in the message payload. When a consumer deserializes, it reads the schema ID prefix, fetches the schema from the registry by ID (cached after first fetch), and uses it to deserialize. Compatibility check is enforced at registration time \u2014 if you try to register a backward-incompatible schema, the registry rejects it.", "Avro separates the schema from the data completely. In JSON, field names ('userId', 'email') are included in every message payload \u2014 redundant when sending millions of messages with the same structure. Avro's binary encoding uses only field positions (indices): the first field in the schema is encoded first, the second field second, with no names written. This makes Avro payloads extremely compact \u2014 often 5-10x smaller than equivalent JSON. The tradeoff: you cannot read an Avro binary payload without the schema. This is why the schema registry is not optional for Avro.", "Incompatible schema migration (renaming a required field, changing a field type) requires a dual-write approach. Step 1: update the producer to write two fields \u2014 the old field name with the old value, and the new field name with the new value. Register this transitional schema, which is backward compatible with consumers expecting the old field. Step 2: update all consumers to read the new field. Step 3: once all consumers are updated, update the producer to write only the new field. Step 4: register the final schema with the old field removed. This is a three-schema migration: original \u2192 transitional (both fields) \u2192 final (new field only)."]}, {"id": 19, "cat": "schema", "diff": "med", "title": "Avro vs. Protobuf vs. JSON for event serialization \u2014 how do you choose?", "answer": "JSON: readable, large, no enforcement. Avro: compact binary, registry-native, Kafka ecosystem. Protobuf: binary, multi-language, no registry required.", "tradeoff": "At 1M msg/sec, JSON is 3-10x larger than binary formats. Choose Avro for Kafka-native, Protobuf for multi-language or gRPC.", "fq": ["What happens when a consumer receives an Avro message but the schema is not in the registry?", "How does Protobuf handle field deletion safely?", "What is a schema fingerprint and how does it work in Avro?"], "fqa": ["If the schema ID embedded in the Avro message does not exist in the registry (schema was deleted, wrong registry URL, or message from a different environment), the consumer's deserializer throws SchemaNotFoundException and the consumer fails to process the message. This is a hard failure \u2014 there is no graceful fallback because the binary payload is unreadable without the schema. Prevention: never delete schemas from the registry (use soft-delete), ensure all environments share the same registry or have synchronized schemas, and set up registry replication for disaster recovery.", "In Protobuf, each field has a unique field number (e.g. field 1 = userId, field 2 = email). The binary encoding uses field numbers, not field names. When a field is deleted, the old field number must never be reused \u2014 mark it as reserved: reserved 2; reserved 'email'; This tells the Protobuf compiler to reject any future field that reuses that number or name, preventing silent data corruption. Old consumers that still know about field 2 will get the default value for fields not present. New consumers that do not know about field 2 will ignore it in old messages.", "A schema fingerprint is a hash (typically MD5, SHA-256, or Rabin fingerprint) of the canonical serialized form of the schema. Avro uses a 64-bit Rabin fingerprint as a compact schema identifier. Instead of sending the full schema in every message, you send a fixed-size fingerprint. The receiver looks up the full schema by fingerprint from a local cache or central store. The SchemaNormalization utility in the Avro Java library computes the fingerprint. Used in environments where you want schema identification without a running registry service."]}, {"id": 20, "cat": "schema", "diff": "hard", "title": "How do you migrate a Kafka topic to a new incompatible schema without downtime?", "answer": "Three phases: dual-write to old and new topic, migrate consumer groups one by one to new topic, decommission old topic after retention period.", "tradeoff": "Dual-write doubles write throughput during migration. Alternative: version envelope in same topic \u2014 simpler but pollutes with mixed schemas.", "fq": ["How do you handle a consumer that reads both old and new messages during migration?", "What is the role of a Kafka Streams topology in schema migration?", "How do you backfill historical events into the new topic format?"], "fqa": ["A consumer that needs to read both old and new messages during migration should use a version discriminator: check the schema ID (for Avro) or a version field in the message envelope, then deserialize using the appropriate schema for that version. Implement this as a polymorphic deserializer: deserialize the schema ID, look it up in a registry or local map, and dispatch to the correct deserialization path. This is temporary code \u2014 once all producers have migrated to the new schema and the retention period for old messages has elapsed, the old deserialization path can be removed.", "A Kafka Streams topology is ideal for schema migration because it is a stateless stream processing pipeline: read from the old topic, transform each message to the new schema, write to the new topic. The topology handles offset management, parallelism, and fault tolerance automatically. Example: KStream input = builder.stream('old-topic'); input.map((k,v) -> new KeyValue(k, transform(v))).to('new-topic'). The topology processes historical messages (replay from offset 0) and new messages in the same pipeline. Kafka Streams exactly-once semantics ensure no message is lost or duplicated in the transformation.", "Backfilling historical events into the new topic format requires replaying the old topic through a transformation job. Options: (1) Kafka Streams job \u2014 read old topic from offset 0, transform, write to new topic. Run it to completion before starting migration phase 2. (2) Kafka consumer + producer script \u2014 simpler for one-time backfill, but you must manage exactly-once semantics manually. (3) Apache Spark or Flink batch job \u2014 for very large backlogs (billions of messages), a distributed batch job processes in parallel across the topic's partitions. After backfill: verify the new topic has the same number of messages as the old topic, then start phase 2 consumer migration."]}, {"id": 21, "cat": "observability", "diff": "hard", "title": "How do you trace a request end-to-end across 5 event-driven services?", "answer": "W3C Trace Context propagation via message headers. Each service extracts trace context on consume and injects it on produce. OpenTelemetry auto-instruments Kafka/SQS clients.", "tradeoff": "Must explicitly propagate at every produce/consume boundary. Missing propagation at one service severs the trace chain.", "fq": ["What is the difference between a trace, a span, and a trace context?", "How do you handle trace sampling in a high-throughput event pipeline?", "What is the W3C traceparent header format?"], "fqa": ["A trace is the complete record of a single request as it flows through multiple services \u2014 the full tree of all operations triggered by that request. A span is one unit of work within a trace \u2014 e.g. 'Service A processing message' or 'DB query in Service B'. Each span has a start time, end time, service name, operation name, status, and attributes. Spans are linked in a parent-child hierarchy to form the trace tree. A trace context is the minimal information needed to link a new span to an existing trace: the trace ID (identifies the trace) and the parent span ID (identifies which span is the parent of the new span).", "Sampling in a high-throughput event pipeline prevents the tracing backend from being overwhelmed by storing only a fraction of traces. Three strategies: (1) Head-based sampling \u2014 the decision to sample is made at the first service and propagated in the trace context. All subsequent services follow the same decision. Simple but may miss important traces that look normal at the start. (2) Tail-based sampling \u2014 collect all spans, make the sampling decision at the end based on the full trace (e.g. always sample traces with errors or high latency). More accurate but requires buffering all spans temporarily. (3) Reservoir sampling \u2014 sample a fixed number of traces per time window regardless of total volume, preserving statistical representation.", "The W3C traceparent header format is: version-traceId-parentId-flags. Example: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01. Version (00) is always 00 in the current spec. TraceId is a 16-byte (32 hex char) globally unique identifier. ParentId is an 8-byte (16 hex char) identifier for the parent span. Flags is a 1-byte bitmask: bit 0 (sampled flag) = 1 means this trace is sampled and backends should record it, 0 means not sampled. The tracestate header carries vendor-specific additional context."]}, {"id": 22, "cat": "observability", "diff": "med", "title": "What metrics do you instrument on an event-driven pipeline and what do you alert on?", "answer": "Producer: error rate, p99 latency. Consumer: lag (growing, not absolute), DLQ depth > 0, error rate > 5%. Broker: under-replicated partitions > 0.", "tradeoff": "Alert on lag rate-of-change, not absolute value. Batch consumers always show high lag between runs \u2014 do not false-alert on them.", "fq": ["How do you alert on consumer lag without false positives from batch consumers?", "What is an under-replicated partition and what causes it?", "How do you instrument a DLQ to distinguish transient from permanent failures?"], "fqa": ["To avoid false-positive lag alerts for batch consumers: use rate-of-change alerting rather than threshold alerting. Alert when the lag delta over the last N minutes is positive AND the lag has been growing continuously for M minutes. For a batch consumer that runs hourly, the lag spikes at the start of each batch run and drops to 0 at the end \u2014 the rate-of-change is not monotonically growing. A stuck consumer shows monotonically growing lag. Alternatively, use schedule-aware alerting: define a maintenance window for the batch consumer's expected run time and suppress lag alerts during that window.", "An under-replicated partition is a partition where fewer than the configured replication.factor replicas are in the ISR. For example, a topic with replication.factor=3 that has only 2 replicas in the ISR is under-replicated. Causes: (1) a broker has gone down, (2) a broker is overloaded or experiencing disk slowness causing followers to fall behind and drop out of the ISR, (3) network partition between brokers preventing replication. Under-replicated partitions > 0 is a P1 alert because it means you are operating with reduced durability. Monitor via kafka.server:type=ReplicaManager,name=UnderReplicatedPartitions JMX metric.", "Instrument the DLQ with metadata that distinguishes failure type. On routing to DLQ, attach to the message: the exception class name, a short error message, the attempt count, and a failure_type tag (PERMANENT or TRANSIENT). PERMANENT: exceptions that indicate the message is malformed or the processing logic has a bug \u2014 NullPointerException on parsing, schema validation failure. TRANSIENT: exceptions that indicate infrastructure issues \u2014 SocketTimeoutException, database connection failure, HTTP 503. Alert separately on PERMANENT DLQ depth growing (bug to fix) vs. TRANSIENT DLQ depth growing (infrastructure issue)."]}, {"id": 23, "cat": "observability", "diff": "hard", "title": "How do you debug a production incident where events are processed but downstream state is wrong?", "answer": "Four steps: confirm delivery, check ordering violations, check duplicate processing, compare state snapshot against event log replay.", "tradeoff": "Replay requires a replay mode that suppresses side effects (emails, payments). Event sourcing makes this debug path tractable by design.", "fq": ["How do you replay a Kafka topic for a specific consumer group without affecting other groups?", "What is event sourcing and how does it relate to this debugging problem?", "How do you handle replaying events that have already triggered external side effects?"], "fqa": ["To replay a Kafka topic for a specific consumer group without affecting other groups, reset the consumer group offset. Each consumer group has its own independent offset pointer per partition \u2014 resetting one group does not affect others. Steps: (1) stop all consumers in the target group, (2) use kafka-consumer-groups.sh --reset-offsets --group target-group --topic my-topic --to-earliest --execute to reset to the beginning, (3) restart the consumers \u2014 they pick up from the reset offset and replay from there. Critical: run with --dry-run first to verify what offset reset would be applied, and ensure the consumer can handle reprocessing idempotently.", "Event sourcing is an architectural pattern where the system's state is derived entirely from an immutable, append-only log of events \u2014 the events are the source of truth, not the current state snapshot. The current state is always a projection computed by replaying events from the beginning. This directly solves the debugging problem: when state is wrong, you replay the event log from the beginning in a debug environment and step through each event to find the point of divergence. Without event sourcing, if the current state is corrupted, there is no authoritative history to replay from.", "External side effects that cannot be undone (emails sent, payments processed) require idempotency keys and a side-effect log. Before triggering the side effect, check an idempotency store (Redis or DB table keyed by event ID + side-effect type): if already triggered, skip. After triggering, record the result in the idempotency store. On replay, the idempotency check prevents re-sending emails or re-charging payments. For side effects that must not be idempotent by design, use a replay mode flag: the consumer checks an environment flag and skips non-idempotent side effects in replay mode."]}, {"id": 24, "cat": "async", "diff": "hard", "title": "When do you choose event-driven async over synchronous REST? What are the tradeoffs?", "answer": "Async: long-running, fan-out, temporal decoupling, reliability under failure. Sync REST: immediate result needed, < 500ms, strong consistency required.", "tradeoff": "Async failure paths (DLQ, poison pills, ordering, dedup) are much harder than sync. Choose async when reliability and decoupling outweigh debugging overhead.", "fq": ["What is temporal coupling and how does async messaging break it?", "How do you implement request-response over a message queue (the correlation ID pattern)?", "When does choreography-based event-driven architecture become an anti-pattern?"], "fqa": ["Temporal coupling is when Service A can only complete its operation if Service B is available and responsive right now. In synchronous REST, if Service B is down or slow, Service A's request fails or blocks. Async messaging breaks temporal coupling: Service A writes to a queue and returns immediately. Service B consumes from the queue whenever it is available \u2014 even if it is temporarily down, the message waits in the queue. Service A and Service B no longer need to be running at the same time. The cost: you lose synchronous feedback \u2014 if Service B fails to process the message, Service A does not know about it immediately.", "See Q25 for the full correlation ID pattern. Summary: client generates a UUID correlation ID, puts a CompletableFuture in pendingMap, sends the request message with corrId and reply-to queue. Server processes and publishes to the reply-to queue with the same corrId. Client's reply listener reads response, looks up corrId in pendingMap, completes the future. Add orTimeout(30, SECONDS) to clean up pending map entries if the server never responds.", "Choreography-based EDA becomes an anti-pattern when: (1) the saga has more than 4-5 steps \u2014 the event chain becomes impossible to visualize or debug, causality spread across multiple topics and services, (2) rollback is required \u2014 implementing compensating events in choreography requires every service to listen for failure events and trigger its own compensation, which quickly becomes a tangle, (3) cross-cutting concerns (timeouts, retries, deadlines) need to be enforced across the entire workflow \u2014 in choreography, each service manages its own timeouts with no central visibility. Signal to switch to orchestration: when you find yourself drawing a sequence diagram to understand your own event system, that diagram should become an orchestrator."]}, {"id": 25, "cat": "async", "diff": "hard", "title": "What is the correlation ID pattern and how do you implement request-response over a message queue?", "answer": "Client sends correlation ID + reply-to queue. Server responds to reply-to queue with same ID. Client matches by ID using a ConcurrentHashMap of CompletableFutures.", "tradeoff": "Adds latency vs. sync REST. CompletableFuture timeout required to prevent memory leaks if server never responds.", "fq": ["How does the correlation ID pattern relate to the request-reply Enterprise Integration Pattern?", "How do you handle the reply queue if the client crashes before receiving the response?", "What is the difference between a correlation ID and a message ID?"], "fqa": ["The request-reply Enterprise Integration Pattern (from Gregor Hohpe's Enterprise Integration Patterns, 2003) is the formal name for the correlation ID pattern. It defines: a Request channel (the queue the client sends to), a Reply channel (the queue the server responds to), a Correlation Identifier (the ID that links request to response), and a Return Address (the reply queue address embedded in the request). Modern implementations add: timeout-based cleanup of pending CompletableFutures, a single shared reply queue per client instance (not per request) to reduce queue proliferation, and using the correlation ID as the trace ID for distributed tracing.", "If the client crashes before receiving the response, the response message sits in the reply-to queue indefinitely (until queue's message retention expires). On client restart, if it uses a durable reply queue (named, not temporary), it can drain pending responses from the queue on startup. However, the CompletableFuture that was waiting for the response is gone \u2014 the client has lost the in-memory context for that request. Options: (1) the client gives up and the caller receives a timeout error \u2014 the caller retries the original request, which must be idempotent, (2) use a temporary queue (auto-deleted when the consumer disconnects) \u2014 the response is lost with the queue.", "A message ID is a unique identifier for a specific message instance \u2014 it identifies the physical message in the queue. It is assigned by the producer or the messaging system and is used for deduplication (have I processed this exact message before?). A correlation ID is a business identifier that groups related messages \u2014 it identifies the logical request-response pair. One request generates one correlation ID; the response carries the same correlation ID. The message ID of the response is different from the message ID of the request, but both share the same correlation ID. In Kafka: the message ID is the topic+partition+offset tuple; the correlation ID is a user-defined header."]}, {"id": 26, "cat": "realworld", "diff": "hard", "title": "What is a poison pill message and how do you handle it?", "answer": "A message that fails on every attempt, halting the consumer group indefinitely. Detect via consecutive failure count. Route to DLQ after N failures with full context. Never silently discard.", "tradeoff": "N=5 with exponential backoff distinguishes poison pill (fails every time) from transient (resolves during backoff window). Alert separately on single poison pill vs. growing DLQ.", "fq": ["How do you prevent a poison pill from blocking an entire Kafka partition?", "What information should you preserve in the DLQ message for future replay?", "How do you test poison pill handling without deploying a broken message to production?"], "fqa": ["In Kafka, a poison pill at offset N blocks the partition because the consumer cannot advance the offset past it. Three strategies: (1) Dead letter topic with manual offset advance: catch the exception after N retries, publish the message to a dead-letter topic, then call consumer.seek(partition, failedOffset + 1) to advance past the poison pill. The partition is unblocked immediately. (2) Error handler with offset commit: configure a SeekToCurrentErrorHandler (Spring Kafka) which seeks back to the failed offset for retry, and after max retries, routes to DLQ and commits the offset. (3) Pause and alert: pause the affected partition and alert on-call.", "Six fields minimum to preserve in the DLQ message: (1) original raw payload bytes \u2014 the exact bytes as received, so you can reprocess with a fixed consumer, (2) exception class and full stack trace from each failure attempt \u2014 plural, to detect if the exception changed across retries, (3) timestamp of each failure attempt, (4) consumer application version and instance ID \u2014 helps correlate with a specific deployment, (5) original message coordinates \u2014 Kafka: topic + partition + offset; SQS: queue URL + message ID, (6) attempt count. Optional but valuable: the processing context at time of failure (user ID, request ID, trace ID). Store DLQ messages in a queryable format (Elasticsearch or S3 + Athena).", "Two testing approaches. Integration test with synthetic poison pill: write a test that produces a message with a payload your consumer rejects consistently \u2014 e.g. a required field set to null or a base64-encoded blob that fails deserialization. Assert that after N retries, the message appears in the DLQ and the consumer continues processing subsequent messages. This test runs in CI against a local Kafka or LocalStack SQS. Chaos test in staging: deploy a consumer with a feature flag (POISON_PILL_SIMULATION_RATE=0.01) that artificially throws a non-transient exception for 1% of messages. Verify DLQ receives the messages, on-call alert fires, and subsequent messages process correctly."]}];
var DG={"delivery": [{"title": "Delivery guarantees comparison + 2-phase commit", "svg": "<svg viewBox=\"0 0 680 240\" xmlns=\"http://www.w3.org/2000/svg\" font-family=\"Inter,monospace\">\n<rect width=\"680\" height=\"240\" fill=\"#0D1117\" rx=\"8\"/>\n<text x=\"340\" y=\"20\" fill=\"#8B949E\" font-size=\"12\" text-anchor=\"middle\" font-weight=\"600\">Delivery Semantics Comparison</text>\n<rect x=\"10\" y=\"30\" width=\"200\" height=\"100\" rx=\"6\" fill=\"#161B22\" stroke=\"#F85149\" stroke-width=\"1\"/>\n<text x=\"110\" y=\"48\" fill=\"#F85149\" font-size=\"11\" text-anchor=\"middle\" font-weight=\"600\">At-Most-Once</text>\n<text x=\"20\" y=\"65\" fill=\"#8B949E\" font-size=\"10\">1. Commit offset</text>\n<text x=\"20\" y=\"80\" fill=\"#8B949E\" font-size=\"10\">2. Process message</text>\n<text x=\"20\" y=\"95\" fill=\"#F85149\" font-size=\"10\">Crash after 1 = LOST</text>\n<text x=\"20\" y=\"110\" fill=\"#8B949E\" font-size=\"10\">Use: metrics, analytics</text>\n<rect x=\"240\" y=\"30\" width=\"200\" height=\"100\" rx=\"6\" fill=\"#161B22\" stroke=\"#D29922\" stroke-width=\"1\"/>\n<text x=\"340\" y=\"48\" fill=\"#D29922\" font-size=\"11\" text-anchor=\"middle\" font-weight=\"600\">At-Least-Once</text>\n<text x=\"250\" y=\"65\" fill=\"#8B949E\" font-size=\"10\">1. Process message</text>\n<text x=\"250\" y=\"80\" fill=\"#8B949E\" font-size=\"10\">2. Commit offset</text>\n<text x=\"250\" y=\"95\" fill=\"#D29922\" font-size=\"10\">Crash after 1 = DUPLICATE</text>\n<text x=\"250\" y=\"110\" fill=\"#3FB950\" font-size=\"10\">Consumer must be idempotent</text>\n<rect x=\"470\" y=\"30\" width=\"200\" height=\"100\" rx=\"6\" fill=\"#161B22\" stroke=\"#3FB950\" stroke-width=\"1\"/>\n<text x=\"570\" y=\"48\" fill=\"#3FB950\" font-size=\"11\" text-anchor=\"middle\" font-weight=\"600\">Exactly-Once (Kafka)</text>\n<text x=\"480\" y=\"65\" fill=\"#8B949E\" font-size=\"10\">PID + seq# dedup at broker</text>\n<text x=\"480\" y=\"80\" fill=\"#8B949E\" font-size=\"10\">transactional.id + 2PC</text>\n<text x=\"480\" y=\"95\" fill=\"#8B949E\" font-size=\"10\">read_committed consumer</text>\n<text x=\"480\" y=\"110\" fill=\"#F85149\" font-size=\"10\">20-30% throughput cost</text>\n<rect x=\"10\" y=\"148\" width=\"660\" height=\"82\" rx=\"6\" fill=\"#161B22\" stroke=\"#BC8CFF\"/>\n<text x=\"340\" y=\"166\" fill=\"#BC8CFF\" font-size=\"11\" text-anchor=\"middle\" font-weight=\"600\">Kafka 2-Phase Commit for Exactly-Once</text>\n<rect x=\"20\" y=\"173\" width=\"90\" height=\"22\" rx=\"3\" fill=\"#21262D\" stroke=\"#3FB950\"/><text x=\"65\" y=\"188\" fill=\"#3FB950\" font-size=\"9\" text-anchor=\"middle\">Producer</text>\n<rect x=\"160\" y=\"173\" width=\"120\" height=\"22\" rx=\"3\" fill=\"#21262D\" stroke=\"#D29922\"/><text x=\"220\" y=\"188\" fill=\"#D29922\" font-size=\"9\" text-anchor=\"middle\">Tx Coordinator</text>\n<rect x=\"340\" y=\"173\" width=\"90\" height=\"22\" rx=\"3\" fill=\"#21262D\" stroke=\"#2F81F7\"/><text x=\"385\" y=\"188\" fill=\"#2F81F7\" font-size=\"9\" text-anchor=\"middle\">Partition A</text>\n<rect x=\"470\" y=\"173\" width=\"90\" height=\"22\" rx=\"3\" fill=\"#21262D\" stroke=\"#2F81F7\"/><text x=\"515\" y=\"188\" fill=\"#2F81F7\" font-size=\"9\" text-anchor=\"middle\">Partition B</text>\n<line x1=\"110\" y1=\"184\" x2=\"158\" y2=\"184\" stroke=\"#BC8CFF\" stroke-width=\"1\" marker-end=\"url(#d1)\"/>\n<line x1=\"280\" y1=\"184\" x2=\"338\" y2=\"184\" stroke=\"#D29922\" stroke-width=\"1\" marker-end=\"url(#d2)\"/>\n<line x1=\"280\" y1=\"184\" x2=\"468\" y2=\"184\" stroke=\"#D29922\" stroke-width=\"1\" marker-end=\"url(#d2)\"/>\n<text x=\"600\" y=\"184\" fill=\"#3FB950\" font-size=\"9\">PREPARE_COMMIT</text>\n<text x=\"600\" y=\"198\" fill=\"#3FB950\" font-size=\"9\">then WriteTxnMarkers</text>\n<text x=\"600\" y=\"212\" fill=\"#8B949E\" font-size=\"9\">consumers see msgs</text>\n<text x=\"600\" y=\"226\" fill=\"#8B949E\" font-size=\"9\">after commit markers</text>\n<defs>\n<marker id=\"d1\" markerWidth=\"6\" markerHeight=\"5\" refX=\"6\" refY=\"2.5\" orient=\"auto\"><polygon points=\"0 0,6 2.5,0 5\" fill=\"#BC8CFF\"/></marker>\n<marker id=\"d2\" markerWidth=\"6\" markerHeight=\"5\" refX=\"6\" refY=\"2.5\" orient=\"auto\"><polygon points=\"0 0,6 2.5,0 5\" fill=\"#D29922\"/></marker>\n</defs>\n</svg>"}], "ordering": [{"title": "Partition key design and ordering boundaries", "svg": "<svg viewBox=\"0 0 680 220\" xmlns=\"http://www.w3.org/2000/svg\" font-family=\"Inter,monospace\">\n<rect width=\"680\" height=\"220\" fill=\"#0D1117\" rx=\"8\"/>\n<text x=\"340\" y=\"20\" fill=\"#8B949E\" font-size=\"12\" text-anchor=\"middle\" font-weight=\"600\">Kafka Ordering \u2014 Partition Key = Ordering Boundary</text>\n<rect x=\"10\" y=\"32\" width=\"90\" height=\"26\" rx=\"4\" fill=\"#161B22\" stroke=\"#3FB950\"/><text x=\"55\" y=\"49\" fill=\"#3FB950\" font-size=\"10\" text-anchor=\"middle\">Producer A</text>\n<rect x=\"10\" y=\"68\" width=\"90\" height=\"26\" rx=\"4\" fill=\"#161B22\" stroke=\"#3FB950\"/><text x=\"55\" y=\"85\" fill=\"#3FB950\" font-size=\"10\" text-anchor=\"middle\">Producer B</text>\n<rect x=\"138\" y=\"32\" width=\"160\" height=\"26\" rx=\"4\" fill=\"#21262D\" stroke=\"#2F81F7\"/><text x=\"218\" y=\"49\" fill=\"#2F81F7\" font-size=\"10\" text-anchor=\"middle\">P0: user_42 events (v1,v2,v3)</text>\n<rect x=\"138\" y=\"68\" width=\"160\" height=\"26\" rx=\"4\" fill=\"#21262D\" stroke=\"#2F81F7\"/><text x=\"218\" y=\"85\" fill=\"#2F81F7\" font-size=\"10\" text-anchor=\"middle\">P1: user_99 events (v1,v2,v3)</text>\n<rect x=\"138\" y=\"104\" width=\"160\" height=\"26\" rx=\"4\" fill=\"#21262D\" stroke=\"#2F81F7\"/><text x=\"218\" y=\"121\" fill=\"#2F81F7\" font-size=\"10\" text-anchor=\"middle\">P2: user_77 events (v1,v2)</text>\n<rect x=\"338\" y=\"32\" width=\"110\" height=\"26\" rx=\"4\" fill=\"#161B22\" stroke=\"#BC8CFF\"/><text x=\"393\" y=\"49\" fill=\"#BC8CFF\" font-size=\"10\" text-anchor=\"middle\">Consumer 0</text>\n<rect x=\"338\" y=\"68\" width=\"110\" height=\"26\" rx=\"4\" fill=\"#161B22\" stroke=\"#BC8CFF\"/><text x=\"393\" y=\"85\" fill=\"#BC8CFF\" font-size=\"10\" text-anchor=\"middle\">Consumer 1</text>\n<rect x=\"338\" y=\"104\" width=\"110\" height=\"26\" rx=\"4\" fill=\"#161B22\" stroke=\"#BC8CFF\"/><text x=\"393\" y=\"121\" fill=\"#BC8CFF\" font-size=\"10\" text-anchor=\"middle\">Consumer 2</text>\n<line x1=\"100\" y1=\"45\" x2=\"136\" y2=\"45\" stroke=\"#3FB950\" stroke-width=\"1.5\" marker-end=\"url(#o1)\"/>\n<line x1=\"100\" y1=\"81\" x2=\"136\" y2=\"81\" stroke=\"#3FB950\" stroke-width=\"1.5\" marker-end=\"url(#o1)\"/>\n<line x1=\"298\" y1=\"45\" x2=\"336\" y2=\"45\" stroke=\"#BC8CFF\" stroke-width=\"1.5\" marker-end=\"url(#o2)\"/>\n<line x1=\"298\" y1=\"81\" x2=\"336\" y2=\"81\" stroke=\"#BC8CFF\" stroke-width=\"1.5\" marker-end=\"url(#o2)\"/>\n<line x1=\"298\" y1=\"117\" x2=\"336\" y2=\"117\" stroke=\"#BC8CFF\" stroke-width=\"1.5\" marker-end=\"url(#o2)\"/>\n<text x=\"340\" y=\"152\" fill=\"#3FB950\" font-size=\"11\" text-anchor=\"middle\" font-weight=\"600\">Within a partition: strict order guaranteed</text>\n<text x=\"340\" y=\"168\" fill=\"#F85149\" font-size=\"11\" text-anchor=\"middle\">Across partitions: NO ordering guarantee</text>\n<rect x=\"10\" y=\"180\" width=\"660\" height=\"30\" rx=\"4\" fill=\"#161B22\" stroke=\"#30363D\"/>\n<text x=\"340\" y=\"196\" fill=\"#D29922\" font-size=\"10\" text-anchor=\"middle\">Breaks ordering: acks=1+leader crash \u00b7 rebalance uncommitted offsets \u00b7 multi-producer race on same key</text>\n<text x=\"340\" y=\"208\" fill=\"#8B949E\" font-size=\"9\" text-anchor=\"middle\">Fix: acks=all + min.insync.replicas=2 + CooperativeStickyAssignor + version numbers in events</text>\n<defs>\n<marker id=\"o1\" markerWidth=\"6\" markerHeight=\"5\" refX=\"6\" refY=\"2.5\" orient=\"auto\"><polygon points=\"0 0,6 2.5,0 5\" fill=\"#3FB950\"/></marker>\n<marker id=\"o2\" markerWidth=\"6\" markerHeight=\"5\" refX=\"6\" refY=\"2.5\" orient=\"auto\"><polygon points=\"0 0,6 2.5,0 5\" fill=\"#BC8CFF\"/></marker>\n</defs>\n</svg>"}], "failure": [{"title": "SQS retry / DLQ / circuit breaker flow", "svg": "<svg viewBox=\"0 0 680 220\" xmlns=\"http://www.w3.org/2000/svg\" font-family=\"Inter,monospace\">\n<rect width=\"680\" height=\"220\" fill=\"#0D1117\" rx=\"8\"/>\n<text x=\"340\" y=\"20\" fill=\"#8B949E\" font-size=\"12\" text-anchor=\"middle\" font-weight=\"600\">SQS Failure Handling \u2014 Retry / Circuit Breaker / DLQ</text>\n<rect x=\"10\" y=\"35\" width=\"80\" height=\"28\" rx=\"4\" fill=\"#161B22\" stroke=\"#3FB950\"/><text x=\"50\" y=\"53\" fill=\"#3FB950\" font-size=\"10\" text-anchor=\"middle\">Producer</text>\n<rect x=\"120\" y=\"35\" width=\"100\" height=\"28\" rx=\"4\" fill=\"#161B22\" stroke=\"#2F81F7\"/><text x=\"170\" y=\"53\" fill=\"#2F81F7\" font-size=\"10\" text-anchor=\"middle\">SQS Queue</text>\n<rect x=\"255\" y=\"35\" width=\"100\" height=\"28\" rx=\"4\" fill=\"#161B22\" stroke=\"#BC8CFF\"/><text x=\"305\" y=\"53\" fill=\"#BC8CFF\" font-size=\"10\" text-anchor=\"middle\">Consumer</text>\n<rect x=\"390\" y=\"35\" width=\"120\" height=\"28\" rx=\"4\" fill=\"#161B22\" stroke=\"#F85149\"/><text x=\"450\" y=\"53\" fill=\"#F85149\" font-size=\"10\" text-anchor=\"middle\">Downstream 30% fail</text>\n<line x1=\"90\" y1=\"49\" x2=\"118\" y2=\"49\" stroke=\"#2F81F7\" stroke-width=\"1.5\" marker-end=\"url(#f1)\"/>\n<line x1=\"220\" y1=\"49\" x2=\"253\" y2=\"49\" stroke=\"#2F81F7\" stroke-width=\"1.5\" marker-end=\"url(#f1)\"/>\n<line x1=\"355\" y1=\"49\" x2=\"388\" y2=\"49\" stroke=\"#F85149\" stroke-width=\"1.5\" marker-end=\"url(#f2)\"/>\n<path d=\"M 355 63 Q 305 88 255 63\" stroke=\"#D29922\" stroke-width=\"1.5\" fill=\"none\" marker-end=\"url(#f3)\" stroke-dasharray=\"4,3\"/>\n<text x=\"305\" y=\"84\" fill=\"#D29922\" font-size=\"9\" text-anchor=\"middle\">exp. backoff + jitter \u2192 requeue</text>\n<rect x=\"530\" y=\"70\" width=\"140\" height=\"28\" rx=\"4\" fill=\"#21262D\" stroke=\"#D29922\"/><text x=\"600\" y=\"84\" fill=\"#D29922\" font-size=\"10\" text-anchor=\"middle\">Circuit Breaker</text>\n<text x=\"600\" y=\"97\" fill=\"#8B949E\" font-size=\"8\" text-anchor=\"middle\">err>25%/30s = OPEN</text>\n<line x1=\"450\" y1=\"63\" x2=\"538\" y2=\"70\" stroke=\"#D29922\" stroke-width=\"1\" stroke-dasharray=\"3,2\" marker-end=\"url(#f3)\"/>\n<rect x=\"120\" y=\"115\" width=\"130\" height=\"28\" rx=\"4\" fill=\"#161B22\" stroke=\"#F85149\"/><text x=\"185\" y=\"133\" fill=\"#F85149\" font-size=\"10\" text-anchor=\"middle\">Dead Letter Queue</text>\n<line x1=\"305\" y1=\"63\" x2=\"240\" y2=\"115\" stroke=\"#F85149\" stroke-width=\"1.5\" marker-end=\"url(#f2)\" stroke-dasharray=\"5,3\"/>\n<text x=\"235\" y=\"105\" fill=\"#8B949E\" font-size=\"8\">maxReceive\u2192</text>\n<rect x=\"10\" y=\"165\" width=\"660\" height=\"44\" rx=\"4\" fill=\"#161B22\" stroke=\"#3FB950\"/>\n<text x=\"340\" y=\"183\" fill=\"#3FB950\" font-size=\"11\" text-anchor=\"middle\" font-weight=\"600\">Controlled DLQ Replay (after service confirmed healthy)</text>\n<text x=\"340\" y=\"199\" fill=\"#8B949E\" font-size=\"10\" text-anchor=\"middle\">1. Verify service healthy 2. Rate-limited re-drive (1% canary first) 3. Monitor error rate 4. Delete on success</text>\n<text x=\"340\" y=\"207\" fill=\"#D29922\" font-size=\"8\" text-anchor=\"middle\">visibility timeout = 6\u00d7 p99 \u00b7 max receive count = 30-min retry window</text>\n<defs>\n<marker id=\"f1\" markerWidth=\"6\" markerHeight=\"5\" refX=\"6\" refY=\"2.5\" orient=\"auto\"><polygon points=\"0 0,6 2.5,0 5\" fill=\"#2F81F7\"/></marker>\n<marker id=\"f2\" markerWidth=\"6\" markerHeight=\"5\" refX=\"6\" refY=\"2.5\" orient=\"auto\"><polygon points=\"0 0,6 2.5,0 5\" fill=\"#F85149\"/></marker>\n<marker id=\"f3\" markerWidth=\"6\" markerHeight=\"5\" refX=\"6\" refY=\"2.5\" orient=\"auto\"><polygon points=\"0 0,6 2.5,0 5\" fill=\"#D29922\"/></marker>\n</defs>\n</svg>"}, {"title": "Outbox pattern \u2014 atomic dual write solution", "svg": "<svg viewBox=\"0 0 680 175\" xmlns=\"http://www.w3.org/2000/svg\" font-family=\"Inter,monospace\">\n<rect width=\"680\" height=\"175\" fill=\"#0D1117\" rx=\"8\"/>\n<text x=\"340\" y=\"20\" fill=\"#8B949E\" font-size=\"12\" text-anchor=\"middle\" font-weight=\"600\">Outbox Pattern \u2014 Solving the Dual-Write Problem</text>\n<rect x=\"10\" y=\"32\" width=\"300\" height=\"80\" rx=\"6\" fill=\"#161B22\" stroke=\"#F85149\"/>\n<text x=\"160\" y=\"50\" fill=\"#F85149\" font-size=\"11\" text-anchor=\"middle\" font-weight=\"600\">Naive approach (broken)</text>\n<rect x=\"20\" y=\"58\" width=\"75\" height=\"22\" rx=\"3\" fill=\"#21262D\"/><text x=\"57\" y=\"73\" fill=\"#8B949E\" font-size=\"9\" text-anchor=\"middle\">App Service</text>\n<rect x=\"110\" y=\"58\" width=\"70\" height=\"22\" rx=\"3\" fill=\"#21262D\"/><text x=\"145\" y=\"73\" fill=\"#8B949E\" font-size=\"9\" text-anchor=\"middle\">DB write</text>\n<rect x=\"195\" y=\"58\" width=\"105\" height=\"22\" rx=\"3\" fill=\"#21262D\"/><text x=\"247\" y=\"73\" fill=\"#8B949E\" font-size=\"9\" text-anchor=\"middle\">Publish event</text>\n<line x1=\"95\" y1=\"69\" x2=\"108\" y2=\"69\" stroke=\"#F85149\" stroke-width=\"1.5\" marker-end=\"url(#b1)\"/>\n<line x1=\"180\" y1=\"69\" x2=\"193\" y2=\"69\" stroke=\"#F85149\" stroke-width=\"1.5\" marker-end=\"url(#b1)\"/>\n<text x=\"160\" y=\"100\" fill=\"#F85149\" font-size=\"9\" text-anchor=\"middle\">crash between steps = event lost</text>\n<rect x=\"330\" y=\"32\" width=\"340\" height=\"80\" rx=\"6\" fill=\"#161B22\" stroke=\"#3FB950\"/>\n<text x=\"500\" y=\"50\" fill=\"#3FB950\" font-size=\"11\" text-anchor=\"middle\" font-weight=\"600\">Outbox pattern (correct)</text>\n<rect x=\"340\" y=\"58\" width=\"75\" height=\"22\" rx=\"3\" fill=\"#21262D\"/><text x=\"377\" y=\"73\" fill=\"#8B949E\" font-size=\"9\" text-anchor=\"middle\">App Service</text>\n<rect x=\"428\" y=\"52\" width=\"100\" height=\"34\" rx=\"3\" fill=\"#0F2A0F\" stroke=\"#3FB950\"/><text x=\"478\" y=\"67\" fill=\"#3FB950\" font-size=\"8\" text-anchor=\"middle\" font-weight=\"600\">ACID Transaction</text><text x=\"478\" y=\"80\" fill=\"#8B949E\" font-size=\"8\" text-anchor=\"middle\">DB + outbox row</text>\n<rect x=\"544\" y=\"58\" width=\"116\" height=\"22\" rx=\"3\" fill=\"#21262D\" stroke=\"#D29922\"/><text x=\"602\" y=\"73\" fill=\"#D29922\" font-size=\"9\" text-anchor=\"middle\">Outbox Worker</text>\n<line x1=\"415\" y1=\"69\" x2=\"426\" y2=\"69\" stroke=\"#3FB950\" stroke-width=\"1.5\" marker-end=\"url(#b2)\"/>\n<line x1=\"528\" y1=\"69\" x2=\"542\" y2=\"69\" stroke=\"#D29922\" stroke-width=\"1.5\" marker-end=\"url(#b3)\"/>\n<text x=\"500\" y=\"100\" fill=\"#8B949E\" font-size=\"9\" text-anchor=\"middle\">polls PENDING \u2192 publish to Kafka \u2192 marks PROCESSED</text>\n<rect x=\"10\" y=\"130\" width=\"660\" height=\"34\" rx=\"4\" fill=\"#21262D\"/>\n<text x=\"340\" y=\"146\" fill=\"#8B949E\" font-size=\"10\" text-anchor=\"middle\">Outbox delivers at-least-once \u2192 consumer must be idempotent. Cleanup: delete PROCESSED rows older than 7 days.</text>\n<text x=\"340\" y=\"159\" fill=\"#D29922\" font-size=\"9\" text-anchor=\"middle\">CDC alternative (Debezium): reads PostgreSQL WAL directly \u2014 zero app changes, but monitor replication slot lag as P1 metric.</text>\n<defs>\n<marker id=\"b1\" markerWidth=\"6\" markerHeight=\"5\" refX=\"6\" refY=\"2.5\" orient=\"auto\"><polygon points=\"0 0,6 2.5,0 5\" fill=\"#F85149\"/></marker>\n<marker id=\"b2\" markerWidth=\"6\" markerHeight=\"5\" refX=\"6\" refY=\"2.5\" orient=\"auto\"><polygon points=\"0 0,6 2.5,0 5\" fill=\"#3FB950\"/></marker>\n<marker id=\"b3\" markerWidth=\"6\" markerHeight=\"5\" refX=\"6\" refY=\"2.5\" orient=\"auto\"><polygon points=\"0 0,6 2.5,0 5\" fill=\"#D29922\"/></marker>\n</defs>\n</svg>"}], "kafka": [{"title": "Kafka high throughput \u2014 four key mechanisms", "svg": "<svg viewBox=\"0 0 680 190\" xmlns=\"http://www.w3.org/2000/svg\" font-family=\"Inter,monospace\">\n<rect width=\"680\" height=\"190\" fill=\"#0D1117\" rx=\"8\"/>\n<text x=\"340\" y=\"20\" fill=\"#8B949E\" font-size=\"12\" text-anchor=\"middle\" font-weight=\"600\">Kafka High Throughput \u2014 Four Key Mechanisms</text>\n<rect x=\"10\" y=\"30\" width=\"150\" height=\"96\" rx=\"6\" fill=\"#161B22\" stroke=\"#2F81F7\"/>\n<text x=\"85\" y=\"48\" fill=\"#2F81F7\" font-size=\"11\" text-anchor=\"middle\" font-weight=\"600\">Sequential Writes</text>\n<text x=\"85\" y=\"64\" fill=\"#8B949E\" font-size=\"9\" text-anchor=\"middle\">Append-only log</text>\n<text x=\"85\" y=\"79\" fill=\"#3FB950\" font-size=\"11\" text-anchor=\"middle\">~500 MB/s seq</text>\n<text x=\"85\" y=\"94\" fill=\"#F85149\" font-size=\"11\" text-anchor=\"middle\">~50 MB/s random</text>\n<text x=\"85\" y=\"110\" fill=\"#8B949E\" font-size=\"9\" text-anchor=\"middle\">10-100x faster I/O</text>\n<rect x=\"178\" y=\"30\" width=\"150\" height=\"96\" rx=\"6\" fill=\"#161B22\" stroke=\"#3FB950\"/>\n<text x=\"253\" y=\"48\" fill=\"#3FB950\" font-size=\"11\" text-anchor=\"middle\" font-weight=\"600\">Zero-Copy</text>\n<text x=\"253\" y=\"64\" fill=\"#8B949E\" font-size=\"9\" text-anchor=\"middle\">OS sendfile() syscall</text>\n<text x=\"253\" y=\"79\" fill=\"#8B949E\" font-size=\"9\" text-anchor=\"middle\">page cache \u2192 NIC</text>\n<text x=\"253\" y=\"94\" fill=\"#3FB950\" font-size=\"11\" text-anchor=\"middle\">60-70% less CPU</text>\n<text x=\"253\" y=\"110\" fill=\"#8B949E\" font-size=\"9\" text-anchor=\"middle\">no user-space copy</text>\n<rect x=\"346\" y=\"30\" width=\"150\" height=\"96\" rx=\"6\" fill=\"#161B22\" stroke=\"#D29922\"/>\n<text x=\"421\" y=\"48\" fill=\"#D29922\" font-size=\"11\" text-anchor=\"middle\" font-weight=\"600\">Batching</text>\n<text x=\"421\" y=\"64\" fill=\"#8B949E\" font-size=\"9\" text-anchor=\"middle\">batch.size + linger.ms</text>\n<text x=\"421\" y=\"79\" fill=\"#8B949E\" font-size=\"9\" text-anchor=\"middle\">compress per batch</text>\n<text x=\"421\" y=\"94\" fill=\"#D29922\" font-size=\"10\" text-anchor=\"middle\">lz4 / zstd</text>\n<text x=\"421\" y=\"110\" fill=\"#8B949E\" font-size=\"9\" text-anchor=\"middle\">amortize per-msg cost</text>\n<rect x=\"514\" y=\"30\" width=\"156\" height=\"96\" rx=\"6\" fill=\"#161B22\" stroke=\"#BC8CFF\"/>\n<text x=\"592\" y=\"48\" fill=\"#BC8CFF\" font-size=\"11\" text-anchor=\"middle\" font-weight=\"600\">Partitioning</text>\n<text x=\"592\" y=\"64\" fill=\"#8B949E\" font-size=\"9\" text-anchor=\"middle\">N partitions \u00d7 M brokers</text>\n<text x=\"592\" y=\"79\" fill=\"#BC8CFF\" font-size=\"11\" text-anchor=\"middle\">linear scale</text>\n<text x=\"592\" y=\"94\" fill=\"#8B949E\" font-size=\"9\" text-anchor=\"middle\">1 partition = 1 consumer</text>\n<text x=\"592\" y=\"110\" fill=\"#8B949E\" font-size=\"9\" text-anchor=\"middle\">can't reduce later</text>\n<rect x=\"10\" y=\"140\" width=\"660\" height=\"40\" rx=\"4\" fill=\"#161B22\" stroke=\"#3FB950\"/>\n<text x=\"340\" y=\"158\" fill=\"#3FB950\" font-size=\"11\" text-anchor=\"middle\" font-weight=\"600\">Result: millions of messages/sec per cluster</text>\n<text x=\"340\" y=\"173\" fill=\"#8B949E\" font-size=\"9\" text-anchor=\"middle\">Page cache tip: give brokers max RAM for OS cache (not JVM heap). Producers write to cache \u2192 recent consumers read from cache = near-zero disk I/O.</text>\n</svg>"}, {"title": "ISR and broker failure \u2014 acks=all durability model", "svg": "<svg viewBox=\"0 0 680 195\" xmlns=\"http://www.w3.org/2000/svg\" font-family=\"Inter,monospace\">\n<rect width=\"680\" height=\"195\" fill=\"#0D1117\" rx=\"8\"/>\n<text x=\"340\" y=\"20\" fill=\"#8B949E\" font-size=\"12\" text-anchor=\"middle\" font-weight=\"600\">Kafka ISR \u2014 Broker Failure Without Data Loss</text>\n<rect x=\"10\" y=\"35\" width=\"80\" height=\"28\" rx=\"4\" fill=\"#161B22\" stroke=\"#3FB950\"/><text x=\"50\" y=\"53\" fill=\"#3FB950\" font-size=\"10\" text-anchor=\"middle\">Producer</text>\n<rect x=\"125\" y=\"28\" width=\"115\" height=\"50\" rx=\"4\" fill=\"#161B22\" stroke=\"#2F81F7\" stroke-width=\"2\"/>\n<text x=\"182\" y=\"46\" fill=\"#2F81F7\" font-size=\"10\" text-anchor=\"middle\" font-weight=\"600\">Leader (Broker 1)</text>\n<text x=\"182\" y=\"60\" fill=\"#3FB950\" font-size=\"9\" text-anchor=\"middle\">ISR member \u2713</text>\n<text x=\"182\" y=\"73\" fill=\"#8B949E\" font-size=\"8\" text-anchor=\"middle\">offset 102 \u2713</text>\n<rect x=\"280\" y=\"28\" width=\"115\" height=\"50\" rx=\"4\" fill=\"#161B22\" stroke=\"#3FB950\"/>\n<text x=\"337\" y=\"46\" fill=\"#3FB950\" font-size=\"10\" text-anchor=\"middle\" font-weight=\"600\">Follower (Broker 2)</text>\n<text x=\"337\" y=\"60\" fill=\"#3FB950\" font-size=\"9\" text-anchor=\"middle\">ISR member \u2713</text>\n<text x=\"337\" y=\"73\" fill=\"#8B949E\" font-size=\"8\" text-anchor=\"middle\">offset 102 \u2713</text>\n<rect x=\"435\" y=\"28\" width=\"115\" height=\"50\" rx=\"4\" fill=\"#161B22\" stroke=\"#F85149\"/>\n<text x=\"492\" y=\"46\" fill=\"#F85149\" font-size=\"10\" text-anchor=\"middle\" font-weight=\"600\">Follower (Broker 3)</text>\n<text x=\"492\" y=\"60\" fill=\"#F85149\" font-size=\"9\" text-anchor=\"middle\">NOT in ISR \u2717</text>\n<text x=\"492\" y=\"73\" fill=\"#8B949E\" font-size=\"8\" text-anchor=\"middle\">offset 100 (lagging)</text>\n<line x1=\"90\" y1=\"49\" x2=\"123\" y2=\"49\" stroke=\"#2F81F7\" stroke-width=\"1.5\" marker-end=\"url(#k1)\"/>\n<line x1=\"240\" y1=\"49\" x2=\"278\" y2=\"49\" stroke=\"#3FB950\" stroke-width=\"1.5\" marker-end=\"url(#k2)\"/>\n<line x1=\"240\" y1=\"53\" x2=\"433\" y2=\"53\" stroke=\"#F85149\" stroke-width=\"1\" stroke-dasharray=\"4,3\" marker-end=\"url(#k3)\"/>\n<text x=\"340\" y=\"100\" fill=\"#D29922\" font-size=\"10\" text-anchor=\"middle\">acks=all waits for ALL ISR members before acking producer</text>\n<text x=\"340\" y=\"115\" fill=\"#8B949E\" font-size=\"10\" text-anchor=\"middle\">Broker 1 fails \u2192 controller elects Broker 2 (ISR) as leader \u2192 no data loss \u2713</text>\n<text x=\"340\" y=\"130\" fill=\"#F85149\" font-size=\"10\" text-anchor=\"middle\">ISR shrinks to 1 + min.insync.replicas=2 \u2192 NotEnoughReplicasException (blocks produce) \u2713</text>\n<rect x=\"10\" y=\"148\" width=\"325\" height=\"36\" rx=\"4\" fill=\"#161B22\" stroke=\"#3FB950\"/>\n<text x=\"172\" y=\"165\" fill=\"#3FB950\" font-size=\"10\" text-anchor=\"middle\" font-weight=\"600\">acks=all + min.insync.replicas=2</text>\n<text x=\"172\" y=\"179\" fill=\"#8B949E\" font-size=\"9\" text-anchor=\"middle\">~1-5ms latency cost \u00b7 survives single broker failure</text>\n<rect x=\"345\" y=\"148\" width=\"325\" height=\"36\" rx=\"4\" fill=\"#161B22\" stroke=\"#F85149\"/>\n<text x=\"507\" y=\"165\" fill=\"#F85149\" font-size=\"10\" text-anchor=\"middle\" font-weight=\"600\">Unclean leader election (avoid!)</text>\n<text x=\"507\" y=\"179\" fill=\"#8B949E\" font-size=\"9\" text-anchor=\"middle\">elects out-of-ISR replica \u2192 silent data loss</text>\n<defs>\n<marker id=\"k1\" markerWidth=\"6\" markerHeight=\"5\" refX=\"6\" refY=\"2.5\" orient=\"auto\"><polygon points=\"0 0,6 2.5,0 5\" fill=\"#2F81F7\"/></marker>\n<marker id=\"k2\" markerWidth=\"6\" markerHeight=\"5\" refX=\"6\" refY=\"2.5\" orient=\"auto\"><polygon points=\"0 0,6 2.5,0 5\" fill=\"#3FB950\"/></marker>\n<marker id=\"k3\" markerWidth=\"6\" markerHeight=\"5\" refX=\"6\" refY=\"2.5\" orient=\"auto\"><polygon points=\"0 0,6 2.5,0 5\" fill=\"#F85149\"/></marker>\n</defs>\n</svg>"}], "patterns": [{"title": "Kafka vs SQS \u2014 decision matrix", "svg": "<svg viewBox=\"0 0 680 210\" xmlns=\"http://www.w3.org/2000/svg\" font-family=\"Inter,monospace\">\n<rect width=\"680\" height=\"210\" fill=\"#0D1117\" rx=\"8\"/>\n<text x=\"340\" y=\"20\" fill=\"#8B949E\" font-size=\"12\" text-anchor=\"middle\" font-weight=\"600\">Kafka vs SQS \u2014 Decision Matrix</text>\n<rect x=\"10\" y=\"30\" width=\"240\" height=\"24\" rx=\"3\" fill=\"#21262D\"/><text x=\"130\" y=\"46\" fill=\"#8B949E\" font-size=\"10\" text-anchor=\"middle\">Criterion</text>\n<rect x=\"256\" y=\"30\" width=\"200\" height=\"24\" rx=\"3\" fill=\"#21262D\" stroke=\"#2F81F7\"/><text x=\"356\" y=\"46\" fill=\"#2F81F7\" font-size=\"10\" text-anchor=\"middle\" font-weight=\"600\">Kafka</text>\n<rect x=\"462\" y=\"30\" width=\"208\" height=\"24\" rx=\"3\" fill=\"#21262D\" stroke=\"#D29922\"/><text x=\"566\" y=\"46\" fill=\"#D29922\" font-size=\"10\" text-anchor=\"middle\" font-weight=\"600\">SQS</text>\n<rect x=\"10\" y=\"58\" width=\"240\" height=\"22\" rx=\"2\" fill=\"#161B22\"/><text x=\"130\" y=\"73\" fill=\"#8B949E\" font-size=\"9\" text-anchor=\"middle\">Message replay</text>\n<rect x=\"256\" y=\"58\" width=\"200\" height=\"22\" rx=\"2\" fill=\"#0D3F1F\"/><text x=\"356\" y=\"73\" fill=\"#3FB950\" font-size=\"9\" text-anchor=\"middle\">\u2713 retain + replay any offset</text>\n<rect x=\"462\" y=\"58\" width=\"208\" height=\"22\" rx=\"2\" fill=\"#3F1B1B\"/><text x=\"566\" y=\"73\" fill=\"#F85149\" font-size=\"9\" text-anchor=\"middle\">\u2717 deleted on consume</text>\n<rect x=\"10\" y=\"84\" width=\"240\" height=\"22\" rx=\"2\" fill=\"#161B22\"/><text x=\"130\" y=\"99\" fill=\"#8B949E\" font-size=\"9\" text-anchor=\"middle\">Multiple consumers</text>\n<rect x=\"256\" y=\"84\" width=\"200\" height=\"22\" rx=\"2\" fill=\"#0D3F1F\"/><text x=\"356\" y=\"99\" fill=\"#3FB950\" font-size=\"9\" text-anchor=\"middle\">\u2713 independent consumer groups</text>\n<rect x=\"462\" y=\"84\" width=\"208\" height=\"22\" rx=\"2\" fill=\"#21262D\"/><text x=\"566\" y=\"99\" fill=\"#D29922\" font-size=\"9\" text-anchor=\"middle\">Partial (SNS fan-out)</text>\n<rect x=\"10\" y=\"110\" width=\"240\" height=\"22\" rx=\"2\" fill=\"#161B22\"/><text x=\"130\" y=\"125\" fill=\"#8B949E\" font-size=\"9\" text-anchor=\"middle\">Ordering guarantee</text>\n<rect x=\"256\" y=\"110\" width=\"200\" height=\"22\" rx=\"2\" fill=\"#0D3F1F\"/><text x=\"356\" y=\"125\" fill=\"#3FB950\" font-size=\"9\" text-anchor=\"middle\">\u2713 per-partition key</text>\n<rect x=\"462\" y=\"110\" width=\"208\" height=\"22\" rx=\"2\" fill=\"#21262D\"/><text x=\"566\" y=\"125\" fill=\"#D29922\" font-size=\"9\" text-anchor=\"middle\">Per msg group (FIFO only)</text>\n<rect x=\"10\" y=\"136\" width=\"240\" height=\"22\" rx=\"2\" fill=\"#161B22\"/><text x=\"130\" y=\"151\" fill=\"#8B949E\" font-size=\"9\" text-anchor=\"middle\">Ops overhead</text>\n<rect x=\"256\" y=\"136\" width=\"200\" height=\"22\" rx=\"2\" fill=\"#3F1B1B\"/><text x=\"356\" y=\"151\" fill=\"#F85149\" font-size=\"9\" text-anchor=\"middle\">High (brokers, tuning)</text>\n<rect x=\"462\" y=\"136\" width=\"208\" height=\"22\" rx=\"2\" fill=\"#0D3F1F\"/><text x=\"566\" y=\"151\" fill=\"#3FB950\" font-size=\"9\" text-anchor=\"middle\">\u2713 zero (fully managed)</text>\n<rect x=\"10\" y=\"162\" width=\"240\" height=\"22\" rx=\"2\" fill=\"#161B22\"/><text x=\"130\" y=\"177\" fill=\"#8B949E\" font-size=\"9\" text-anchor=\"middle\">Throughput ceiling</text>\n<rect x=\"256\" y=\"162\" width=\"200\" height=\"22\" rx=\"2\" fill=\"#0D3F1F\"/><text x=\"356\" y=\"177\" fill=\"#3FB950\" font-size=\"9\" text-anchor=\"middle\">\u2713 millions/sec per cluster</text>\n<rect x=\"462\" y=\"162\" width=\"208\" height=\"22\" rx=\"2\" fill=\"#21262D\"/><text x=\"566\" y=\"177\" fill=\"#D29922\" font-size=\"9\" text-anchor=\"middle\">3K/s FIFO, unlimited Standard</text>\n<rect x=\"10\" y=\"190\" width=\"660\" height=\"16\" rx=\"3\" fill=\"#3F1B1B\" stroke=\"#F85149\" stroke-width=\"0.5\"/>\n<text x=\"340\" y=\"202\" fill=\"#F85149\" font-size=\"10\" text-anchor=\"middle\" font-weight=\"600\">Key decision: SQS has no replay. If ANY downstream needs event history \u2192 Kafka.</text>\n</svg>"}], "backpressure": [{"title": "Token bucket vs sliding window rate limiting", "svg": "<svg viewBox=\"0 0 680 200\" xmlns=\"http://www.w3.org/2000/svg\" font-family=\"Inter,monospace\">\n<rect width=\"680\" height=\"200\" fill=\"#0D1117\" rx=\"8\"/>\n<text x=\"340\" y=\"20\" fill=\"#8B949E\" font-size=\"12\" text-anchor=\"middle\" font-weight=\"600\">Rate Limiting \u2014 Token Bucket vs Sliding Window</text>\n<rect x=\"10\" y=\"32\" width=\"320\" height=\"108\" rx=\"6\" fill=\"#161B22\" stroke=\"#E3B341\"/>\n<text x=\"170\" y=\"50\" fill=\"#E3B341\" font-size=\"11\" text-anchor=\"middle\" font-weight=\"600\">Token Bucket</text>\n<rect x=\"60\" y=\"58\" width=\"100\" height=\"55\" rx=\"4\" fill=\"#21262D\" stroke=\"#E3B341\"/>\n<text x=\"110\" y=\"74\" fill=\"#E3B341\" font-size=\"9\" text-anchor=\"middle\">bucket (cap=100)</text>\n<rect x=\"66\" y=\"79\" width=\"88\" height=\"10\" rx=\"2\" fill=\"#E3B341\" opacity=\"0.8\"/>\n<rect x=\"66\" y=\"93\" width=\"60\" height=\"10\" rx=\"2\" fill=\"#E3B341\" opacity=\"0.4\"/>\n<text x=\"110\" y=\"112\" fill=\"#8B949E\" font-size=\"8\" text-anchor=\"middle\">refills at rate</text>\n<text x=\"210\" y=\"68\" fill=\"#3FB950\" font-size=\"10\">\u2713 allows burst spending</text>\n<text x=\"210\" y=\"83\" fill=\"#3FB950\" font-size=\"10\">\u2713 idle = accumulate tokens</text>\n<text x=\"210\" y=\"98\" fill=\"#D29922\" font-size=\"10\">Use: bursty-infrequent</text>\n<text x=\"210\" y=\"113\" fill=\"#8B949E\" font-size=\"9\">e.g. batch jobs</text>\n<text x=\"170\" y=\"133\" fill=\"#8B949E\" font-size=\"9\" text-anchor=\"middle\">Guava RateLimiter.create(100.0)</text>\n<rect x=\"350\" y=\"32\" width=\"320\" height=\"108\" rx=\"6\" fill=\"#161B22\" stroke=\"#BC8CFF\"/>\n<text x=\"510\" y=\"50\" fill=\"#BC8CFF\" font-size=\"11\" text-anchor=\"middle\" font-weight=\"600\">Sliding Window Counter</text>\n<rect x=\"360\" y=\"60\" width=\"280\" height=\"28\" rx=\"3\" fill=\"#21262D\"/>\n<rect x=\"360\" y=\"60\" width=\"130\" height=\"28\" rx=\"3\" fill=\"#BC8CFF\" opacity=\"0.3\"/>\n<text x=\"425\" y=\"78\" fill=\"#fff\" font-size=\"8\" text-anchor=\"middle\">prev window (70 reqs)</text>\n<rect x=\"490\" y=\"60\" width=\"150\" height=\"28\" rx=\"3\" fill=\"#BC8CFF\" opacity=\"0.6\"/>\n<text x=\"565\" y=\"78\" fill=\"#fff\" font-size=\"8\" text-anchor=\"middle\">current (20 reqs)</text>\n<text x=\"510\" y=\"104\" fill=\"#8B949E\" font-size=\"9\" text-anchor=\"middle\">estimate = prev\u00d7(1-t/w) + current</text>\n<text x=\"510\" y=\"118\" fill=\"#D29922\" font-size=\"10\" text-anchor=\"middle\">Use: mirror external API limits</text>\n<text x=\"510\" y=\"131\" fill=\"#8B949E\" font-size=\"9\" text-anchor=\"middle\">Redis Lua: atomic check+INCR</text>\n<rect x=\"10\" y=\"152\" width=\"660\" height=\"38\" rx=\"4\" fill=\"#161B22\" stroke=\"#30363D\"/>\n<text x=\"340\" y=\"170\" fill=\"#D29922\" font-size=\"11\" text-anchor=\"middle\" font-weight=\"600\">Redis Lua eliminates race condition</text>\n<text x=\"340\" y=\"184\" fill=\"#8B949E\" font-size=\"9\" text-anchor=\"middle\">GET+check+INCR runs atomically \u2014 no interleave between check and increment. Redis down: fail-open (availability) vs fail-closed (protect external API) = product decision.</text>\n</svg>"}], "schema": [{"title": "Schema compatibility matrix", "svg": "<svg viewBox=\"0 0 680 215\" xmlns=\"http://www.w3.org/2000/svg\" font-family=\"Inter,monospace\">\n<rect width=\"680\" height=\"215\" fill=\"#0D1117\" rx=\"8\"/>\n<text x=\"340\" y=\"20\" fill=\"#8B949E\" font-size=\"12\" text-anchor=\"middle\" font-weight=\"600\">Schema Compatibility \u2014 What Changes Are Safe</text>\n<rect x=\"10\" y=\"30\" width=\"230\" height=\"24\" rx=\"3\" fill=\"#21262D\"/><text x=\"125\" y=\"46\" fill=\"#8B949E\" font-size=\"10\" text-anchor=\"middle\">Change type</text>\n<rect x=\"246\" y=\"30\" width=\"130\" height=\"24\" rx=\"3\" fill=\"#21262D\" stroke=\"#3FB950\"/><text x=\"311\" y=\"46\" fill=\"#3FB950\" font-size=\"10\" text-anchor=\"middle\">Backward</text>\n<rect x=\"382\" y=\"30\" width=\"130\" height=\"24\" rx=\"3\" fill=\"#21262D\" stroke=\"#2F81F7\"/><text x=\"447\" y=\"46\" fill=\"#2F81F7\" font-size=\"10\" text-anchor=\"middle\">Forward</text>\n<rect x=\"518\" y=\"30\" width=\"152\" height=\"24\" rx=\"3\" fill=\"#21262D\" stroke=\"#BC8CFF\"/><text x=\"594\" y=\"46\" fill=\"#BC8CFF\" font-size=\"10\" text-anchor=\"middle\">Full (both)</text>\n<rect x=\"10\" y=\"58\" width=\"230\" height=\"22\" rx=\"2\" fill=\"#161B22\"/><text x=\"125\" y=\"73\" fill=\"#E6EDF3\" font-size=\"9\" text-anchor=\"middle\">Add optional field (with default)</text>\n<rect x=\"246\" y=\"58\" width=\"130\" height=\"22\" rx=\"2\" fill=\"#0D3F1F\"/><text x=\"311\" y=\"74\" fill=\"#3FB950\" font-size=\"14\" text-anchor=\"middle\">\u2713</text>\n<rect x=\"382\" y=\"58\" width=\"130\" height=\"22\" rx=\"2\" fill=\"#0D3F1F\"/><text x=\"447\" y=\"74\" fill=\"#3FB950\" font-size=\"14\" text-anchor=\"middle\">\u2713</text>\n<rect x=\"518\" y=\"58\" width=\"152\" height=\"22\" rx=\"2\" fill=\"#0D3F1F\"/><text x=\"594\" y=\"74\" fill=\"#3FB950\" font-size=\"14\" text-anchor=\"middle\">\u2713</text>\n<rect x=\"10\" y=\"84\" width=\"230\" height=\"22\" rx=\"2\" fill=\"#161B22\"/><text x=\"125\" y=\"99\" fill=\"#E6EDF3\" font-size=\"9\" text-anchor=\"middle\">Remove optional field</text>\n<rect x=\"246\" y=\"84\" width=\"130\" height=\"22\" rx=\"2\" fill=\"#0D3F1F\"/><text x=\"311\" y=\"100\" fill=\"#3FB950\" font-size=\"14\" text-anchor=\"middle\">\u2713</text>\n<rect x=\"382\" y=\"84\" width=\"130\" height=\"22\" rx=\"2\" fill=\"#3F1B1B\"/><text x=\"447\" y=\"100\" fill=\"#F85149\" font-size=\"14\" text-anchor=\"middle\">\u2717</text>\n<rect x=\"518\" y=\"84\" width=\"152\" height=\"22\" rx=\"2\" fill=\"#3F1B1B\"/><text x=\"594\" y=\"100\" fill=\"#F85149\" font-size=\"14\" text-anchor=\"middle\">\u2717</text>\n<rect x=\"10\" y=\"110\" width=\"230\" height=\"22\" rx=\"2\" fill=\"#161B22\"/><text x=\"125\" y=\"125\" fill=\"#E6EDF3\" font-size=\"9\" text-anchor=\"middle\">Add required field</text>\n<rect x=\"246\" y=\"110\" width=\"130\" height=\"22\" rx=\"2\" fill=\"#3F1B1B\"/><text x=\"311\" y=\"126\" fill=\"#F85149\" font-size=\"14\" text-anchor=\"middle\">\u2717</text>\n<rect x=\"382\" y=\"110\" width=\"130\" height=\"22\" rx=\"2\" fill=\"#0D3F1F\"/><text x=\"447\" y=\"126\" fill=\"#3FB950\" font-size=\"14\" text-anchor=\"middle\">\u2713</text>\n<rect x=\"518\" y=\"110\" width=\"152\" height=\"22\" rx=\"2\" fill=\"#3F1B1B\"/><text x=\"594\" y=\"126\" fill=\"#F85149\" font-size=\"14\" text-anchor=\"middle\">\u2717</text>\n<rect x=\"10\" y=\"136\" width=\"230\" height=\"22\" rx=\"2\" fill=\"#161B22\"/><text x=\"125\" y=\"151\" fill=\"#E6EDF3\" font-size=\"9\" text-anchor=\"middle\">Rename or change field type</text>\n<rect x=\"246\" y=\"136\" width=\"130\" height=\"22\" rx=\"2\" fill=\"#3F1B1B\"/><text x=\"311\" y=\"152\" fill=\"#F85149\" font-size=\"14\" text-anchor=\"middle\">\u2717</text>\n<rect x=\"382\" y=\"136\" width=\"130\" height=\"22\" rx=\"2\" fill=\"#3F1B1B\"/><text x=\"447\" y=\"152\" fill=\"#F85149\" font-size=\"14\" text-anchor=\"middle\">\u2717</text>\n<rect x=\"518\" y=\"136\" width=\"152\" height=\"22\" rx=\"2\" fill=\"#3F1B1B\"/><text x=\"594\" y=\"152\" fill=\"#F85149\" font-size=\"14\" text-anchor=\"middle\">\u2717</text>\n<rect x=\"10\" y=\"172\" width=\"660\" height=\"34\" rx=\"4\" fill=\"#161B22\" stroke=\"#BC8CFF\"/>\n<text x=\"340\" y=\"189\" fill=\"#BC8CFF\" font-size=\"11\" text-anchor=\"middle\" font-weight=\"600\">Golden rule: only add optional fields with defaults for full compatibility</text>\n<text x=\"340\" y=\"203\" fill=\"#8B949E\" font-size=\"9\" text-anchor=\"middle\">Avro/Protobuf schema registries enforce this at registration time \u2014 incompatible schemas are rejected before any producer can publish</text>\n</svg>"}], "observability": [{"title": "Distributed trace propagation through async services", "svg": "<svg viewBox=\"0 0 680 210\" xmlns=\"http://www.w3.org/2000/svg\" font-family=\"Inter,monospace\">\n<rect width=\"680\" height=\"210\" fill=\"#0D1117\" rx=\"8\"/>\n<text x=\"340\" y=\"20\" fill=\"#8B949E\" font-size=\"12\" text-anchor=\"middle\" font-weight=\"600\">Distributed Trace Propagation \u2014 Event-Driven Services</text>\n<rect x=\"10\" y=\"35\" width=\"108\" height=\"52\" rx=\"6\" fill=\"#161B22\" stroke=\"#39D353\" stroke-width=\"1.5\"/>\n<text x=\"64\" y=\"53\" fill=\"#39D353\" font-size=\"11\" text-anchor=\"middle\" font-weight=\"600\">Service A</text>\n<text x=\"64\" y=\"67\" fill=\"#8B949E\" font-size=\"9\" text-anchor=\"middle\">trace: abc123</text>\n<text x=\"64\" y=\"80\" fill=\"#8B949E\" font-size=\"9\" text-anchor=\"middle\">span: s001 (root)</text>\n<rect x=\"140\" y=\"40\" width=\"130\" height=\"42\" rx=\"4\" fill=\"#21262D\" stroke=\"#2F81F7\"/>\n<text x=\"205\" y=\"57\" fill=\"#2F81F7\" font-size=\"10\" text-anchor=\"middle\" font-weight=\"600\">Kafka headers</text>\n<text x=\"205\" y=\"72\" fill=\"#D29922\" font-size=\"8\" text-anchor=\"middle\">traceparent: 00-abc123-s001-01</text>\n<line x1=\"118\" y1=\"61\" x2=\"138\" y2=\"61\" stroke=\"#39D353\" stroke-width=\"1.5\" marker-end=\"url(#t1)\"/>\n<line x1=\"270\" y1=\"61\" x2=\"288\" y2=\"61\" stroke=\"#39D353\" stroke-width=\"1.5\" marker-end=\"url(#t1)\"/>\n<rect x=\"290\" y=\"35\" width=\"108\" height=\"52\" rx=\"6\" fill=\"#161B22\" stroke=\"#39D353\" stroke-width=\"1.5\"/>\n<text x=\"344\" y=\"53\" fill=\"#39D353\" font-size=\"11\" text-anchor=\"middle\" font-weight=\"600\">Service B</text>\n<text x=\"344\" y=\"67\" fill=\"#8B949E\" font-size=\"9\" text-anchor=\"middle\">trace: abc123 (same)</text>\n<text x=\"344\" y=\"80\" fill=\"#8B949E\" font-size=\"9\" text-anchor=\"middle\">span: s002, parent: s001</text>\n<rect x=\"420\" y=\"40\" width=\"130\" height=\"42\" rx=\"4\" fill=\"#21262D\" stroke=\"#2F81F7\"/>\n<text x=\"485\" y=\"57\" fill=\"#2F81F7\" font-size=\"10\" text-anchor=\"middle\" font-weight=\"600\">SQS attributes</text>\n<text x=\"485\" y=\"72\" fill=\"#D29922\" font-size=\"8\" text-anchor=\"middle\">traceparent: 00-abc123-s002-01</text>\n<line x1=\"398\" y1=\"61\" x2=\"418\" y2=\"61\" stroke=\"#39D353\" stroke-width=\"1.5\" marker-end=\"url(#t1)\"/>\n<line x1=\"550\" y1=\"61\" x2=\"568\" y2=\"61\" stroke=\"#39D353\" stroke-width=\"1.5\" marker-end=\"url(#t1)\"/>\n<rect x=\"570\" y=\"35\" width=\"100\" height=\"52\" rx=\"6\" fill=\"#161B22\" stroke=\"#39D353\" stroke-width=\"1.5\"/>\n<text x=\"620\" y=\"53\" fill=\"#39D353\" font-size=\"11\" text-anchor=\"middle\" font-weight=\"600\">Service C</text>\n<text x=\"620\" y=\"67\" fill=\"#8B949E\" font-size=\"9\" text-anchor=\"middle\">trace: abc123</text>\n<text x=\"620\" y=\"80\" fill=\"#8B949E\" font-size=\"9\" text-anchor=\"middle\">span: s003</text>\n<rect x=\"10\" y=\"102\" width=\"660\" height=\"46\" rx=\"6\" fill=\"#161B22\" stroke=\"#30363D\"/>\n<text x=\"340\" y=\"120\" fill=\"#39D353\" font-size=\"11\" text-anchor=\"middle\" font-weight=\"600\">Trace tree in Jaeger/Datadog (all under traceId: abc123)</text>\n<rect x=\"20\" y=\"126\" width=\"140\" height=\"16\" rx=\"3\" fill=\"#21262D\" stroke=\"#39D353\"/><text x=\"90\" y=\"138\" fill=\"#39D353\" font-size=\"8\" text-anchor=\"middle\">A: s001 (root) 50ms</text>\n<rect x=\"168\" y=\"126\" width=\"155\" height=\"16\" rx=\"3\" fill=\"#21262D\" stroke=\"#2F81F7\"/><text x=\"245\" y=\"138\" fill=\"#2F81F7\" font-size=\"8\" text-anchor=\"middle\">B: s002 parent=s001 80ms</text>\n<rect x=\"331\" y=\"126\" width=\"140\" height=\"16\" rx=\"3\" fill=\"#21262D\" stroke=\"#BC8CFF\"/><text x=\"401\" y=\"138\" fill=\"#BC8CFF\" font-size=\"8\" text-anchor=\"middle\">C: s003 parent=s002 40ms</text>\n<text x=\"530\" y=\"138\" fill=\"#8B949E\" font-size=\"8\">total: 170ms</text>\n<rect x=\"10\" y=\"158\" width=\"660\" height=\"42\" rx=\"4\" fill=\"#21262D\"/>\n<text x=\"340\" y=\"174\" fill=\"#D29922\" font-size=\"11\" text-anchor=\"middle\" font-weight=\"600\">W3C traceparent: 00-{traceId 32hex}-{parentId 16hex}-{flags 01=sampled}</text>\n<text x=\"340\" y=\"188\" fill=\"#8B949E\" font-size=\"9\" text-anchor=\"middle\">OpenTelemetry auto-instruments Kafka and SQS clients \u2014 inject on produce, extract on consume automatically</text>\n<text x=\"340\" y=\"200\" fill=\"#F85149\" font-size=\"8\" text-anchor=\"middle\">Missing propagation at ONE service severs the trace chain \u2014 use auto-instrumentation, never rely on manual injection</text>\n<defs>\n<marker id=\"t1\" markerWidth=\"6\" markerHeight=\"5\" refX=\"6\" refY=\"2.5\" orient=\"auto\"><polygon points=\"0 0,6 2.5,0 5\" fill=\"#39D353\"/></marker>\n</defs>\n</svg>"}], "async": [{"title": "Correlation ID pattern \u2014 request-response over a queue", "svg": "<svg viewBox=\"0 0 680 195\" xmlns=\"http://www.w3.org/2000/svg\" font-family=\"Inter,monospace\">\n<rect width=\"680\" height=\"195\" fill=\"#0D1117\" rx=\"8\"/>\n<text x=\"340\" y=\"20\" fill=\"#8B949E\" font-size=\"12\" text-anchor=\"middle\" font-weight=\"600\">Correlation ID Pattern \u2014 Request-Response Over Queue</text>\n<rect x=\"10\" y=\"35\" width=\"90\" height=\"50\" rx=\"6\" fill=\"#161B22\" stroke=\"#3FB950\" stroke-width=\"1.5\"/>\n<text x=\"55\" y=\"53\" fill=\"#3FB950\" font-size=\"11\" text-anchor=\"middle\" font-weight=\"600\">Client</text>\n<text x=\"55\" y=\"67\" fill=\"#8B949E\" font-size=\"8\" text-anchor=\"middle\">Map<corrId,</text>\n<text x=\"55\" y=\"79\" fill=\"#8B949E\" font-size=\"8\" text-anchor=\"middle\">CompletableFuture></text>\n<rect x=\"138\" y=\"35\" width=\"120\" height=\"28\" rx=\"4\" fill=\"#161B22\" stroke=\"#2F81F7\"/><text x=\"198\" y=\"53\" fill=\"#2F81F7\" font-size=\"10\" text-anchor=\"middle\">Request Queue</text>\n<rect x=\"298\" y=\"35\" width=\"90\" height=\"50\" rx=\"6\" fill=\"#161B22\" stroke=\"#BC8CFF\" stroke-width=\"1.5\"/>\n<text x=\"343\" y=\"53\" fill=\"#BC8CFF\" font-size=\"11\" text-anchor=\"middle\" font-weight=\"600\">Server</text>\n<text x=\"343\" y=\"67\" fill=\"#8B949E\" font-size=\"8\" text-anchor=\"middle\">process</text>\n<text x=\"343\" y=\"79\" fill=\"#8B949E\" font-size=\"8\" text-anchor=\"middle\">request</text>\n<rect x=\"138\" y=\"118\" width=\"120\" height=\"28\" rx=\"4\" fill=\"#161B22\" stroke=\"#D29922\"/><text x=\"198\" y=\"136\" fill=\"#D29922\" font-size=\"10\" text-anchor=\"middle\">Reply Queue</text>\n<line x1=\"100\" y1=\"49\" x2=\"136\" y2=\"49\" stroke=\"#2F81F7\" stroke-width=\"1.5\" marker-end=\"url(#ac1)\"/>\n<text x=\"118\" y=\"44\" fill=\"#8B949E\" font-size=\"7\">1. send(corrId+replyTo)</text>\n<line x1=\"258\" y1=\"49\" x2=\"296\" y2=\"49\" stroke=\"#2F81F7\" stroke-width=\"1.5\" marker-end=\"url(#ac1)\"/>\n<text x=\"277\" y=\"44\" fill=\"#8B949E\" font-size=\"7\">2. consume</text>\n<line x1=\"343\" y1=\"85\" x2=\"265\" y2=\"118\" stroke=\"#D29922\" stroke-width=\"1.5\" marker-end=\"url(#ac2)\"/>\n<text x=\"325\" y=\"108\" fill=\"#D29922\" font-size=\"7\">3. publish(corrId+result)</text>\n<line x1=\"138\" y1=\"132\" x2=\"100\" y2=\"75\" stroke=\"#D29922\" stroke-width=\"1.5\" marker-end=\"url(#ac3)\"/>\n<text x=\"95\" y=\"108\" fill=\"#3FB950\" font-size=\"7\" text-anchor=\"middle\">4. match corrId</text>\n<text x=\"95\" y=\"118\" fill=\"#3FB950\" font-size=\"7\" text-anchor=\"middle\">complete(result)</text>\n<rect x=\"436\" y=\"32\" width=\"234\" height=\"98\" rx=\"6\" fill=\"#161B22\" stroke=\"#30363D\"/>\n<text x=\"553\" y=\"50\" fill=\"#8B949E\" font-size=\"10\" text-anchor=\"middle\" font-weight=\"600\">Key design points</text>\n<text x=\"446\" y=\"65\" fill=\"#8B949E\" font-size=\"9\">One reply queue per client (not per request)</text>\n<text x=\"446\" y=\"79\" fill=\"#8B949E\" font-size=\"9\">corrId = UUID = also the trace ID</text>\n<text x=\"446\" y=\"93\" fill=\"#D29922\" font-size=\"9\">CompletableFuture.orTimeout(30s)</text>\n<text x=\"446\" y=\"107\" fill=\"#D29922\" font-size=\"9\">timeout removes map entry (no leak)</text>\n<text x=\"446\" y=\"121\" fill=\"#8B949E\" font-size=\"8\">Adds ~1 hop latency vs REST</text>\n<text x=\"446\" y=\"131\" fill=\"#8B949E\" font-size=\"8\">corrId (business) != msg ID (transport)</text>\n<rect x=\"10\" y=\"158\" width=\"660\" height=\"28\" rx=\"4\" fill=\"#21262D\"/>\n<text x=\"340\" y=\"172\" fill=\"#8B949E\" font-size=\"9\" text-anchor=\"middle\">Client crash before receiving: response orphaned in reply queue. Caller timed out \u2192 retries original request (must be idempotent).</text>\n<text x=\"340\" y=\"184\" fill=\"#8B949E\" font-size=\"8\" text-anchor=\"middle\">EIP name: Request-Reply pattern. corrId is the Correlation Identifier. replyTo is the Return Address.</text>\n<defs>\n<marker id=\"ac1\" markerWidth=\"6\" markerHeight=\"5\" refX=\"6\" refY=\"2.5\" orient=\"auto\"><polygon points=\"0 0,6 2.5,0 5\" fill=\"#2F81F7\"/></marker>\n<marker id=\"ac2\" markerWidth=\"6\" markerHeight=\"5\" refX=\"6\" refY=\"2.5\" orient=\"auto\"><polygon points=\"0 0,6 2.5,0 5\" fill=\"#D29922\"/></marker>\n<marker id=\"ac3\" markerWidth=\"6\" markerHeight=\"5\" refX=\"6\" refY=\"2.5\" orient=\"auto\"><polygon points=\"0 0,6 2.5,0 5\" fill=\"#3FB950\"/></marker>\n</defs>\n</svg>"}], "realworld": [{"title": "Poison pill \u2014 detection, partition unblocking, DLQ contents", "svg": "<svg viewBox=\"0 0 680 230\" xmlns=\"http://www.w3.org/2000/svg\" font-family=\"Inter,monospace\">\n<rect width=\"680\" height=\"230\" fill=\"#0D1117\" rx=\"8\"/>\n<text x=\"340\" y=\"20\" fill=\"#8B949E\" font-size=\"12\" text-anchor=\"middle\" font-weight=\"600\">Poison Pill \u2014 Detection and Handling</text>\n<rect x=\"10\" y=\"35\" width=\"90\" height=\"28\" rx=\"4\" fill=\"#161B22\" stroke=\"#2F81F7\"/><text x=\"55\" y=\"53\" fill=\"#2F81F7\" font-size=\"10\" text-anchor=\"middle\">Source Queue</text>\n<rect x=\"140\" y=\"35\" width=\"90\" height=\"28\" rx=\"4\" fill=\"#161B22\" stroke=\"#BC8CFF\"/><text x=\"185\" y=\"53\" fill=\"#BC8CFF\" font-size=\"10\" text-anchor=\"middle\">Consumer</text>\n<line x1=\"100\" y1=\"49\" x2=\"138\" y2=\"49\" stroke=\"#2F81F7\" stroke-width=\"1.5\" marker-end=\"url(#pp1)\"/>\n<rect x=\"275\" y=\"30\" width=\"140\" height=\"38\" rx=\"4\" fill=\"#21262D\" stroke=\"#D29922\"/>\n<text x=\"345\" y=\"46\" fill=\"#D29922\" font-size=\"10\" text-anchor=\"middle\" font-weight=\"600\">Failure Counter</text>\n<text x=\"345\" y=\"61\" fill=\"#8B949E\" font-size=\"8\" text-anchor=\"middle\">Redis: msgId \u2192 {count, exceptions}</text>\n<line x1=\"230\" y1=\"49\" x2=\"273\" y2=\"49\" stroke=\"#D29922\" stroke-width=\"1\" marker-end=\"url(#pp2)\"/>\n<rect x=\"460\" y=\"30\" width=\"110\" height=\"38\" rx=\"4\" fill=\"#21262D\" stroke=\"#BC8CFF\"/>\n<text x=\"515\" y=\"46\" fill=\"#BC8CFF\" font-size=\"10\" text-anchor=\"middle\">count >= 5?</text>\n<text x=\"515\" y=\"61\" fill=\"#8B949E\" font-size=\"8\" text-anchor=\"middle\">same exception?</text>\n<line x1=\"415\" y1=\"49\" x2=\"458\" y2=\"49\" stroke=\"#BC8CFF\" stroke-width=\"1\" marker-end=\"url(#pp3)\"/>\n<rect x=\"606\" y=\"30\" width=\"64\" height=\"38\" rx=\"4\" fill=\"#21262D\" stroke=\"#3FB950\"/>\n<text x=\"638\" y=\"48\" fill=\"#3FB950\" font-size=\"9\" text-anchor=\"middle\">NO</text>\n<text x=\"638\" y=\"62\" fill=\"#8B949E\" font-size=\"8\" text-anchor=\"middle\">retry</text>\n<line x1=\"570\" y1=\"49\" x2=\"604\" y2=\"49\" stroke=\"#3FB950\" stroke-width=\"1\" marker-end=\"url(#pp4)\"/>\n<rect x=\"275\" y=\"98\" width=\"130\" height=\"28\" rx=\"4\" fill=\"#161B22\" stroke=\"#F85149\"/>\n<text x=\"340\" y=\"116\" fill=\"#F85149\" font-size=\"10\" text-anchor=\"middle\">Dead Letter Queue</text>\n<line x1=\"515\" y1=\"68\" x2=\"406\" y2=\"98\" stroke=\"#F85149\" stroke-width=\"1.5\" marker-end=\"url(#pp5)\"/>\n<text x=\"465\" y=\"90\" fill=\"#F85149\" font-size=\"8\">YES \u2192 DLQ</text>\n<rect x=\"440\" y=\"98\" width=\"230\" height=\"28\" rx=\"4\" fill=\"#21262D\" stroke=\"#D29922\"/>\n<text x=\"555\" y=\"112\" fill=\"#D29922\" font-size=\"9\" text-anchor=\"middle\">Kafka: seek(partition, offset+1)</text>\n<text x=\"555\" y=\"122\" fill=\"#8B949E\" font-size=\"7\" text-anchor=\"middle\">\u2192 unblocks partition immediately</text>\n<line x1=\"405\" y1=\"112\" x2=\"438\" y2=\"112\" stroke=\"#D29922\" stroke-width=\"1\" marker-end=\"url(#pp2)\"/>\n<rect x=\"10\" y=\"144\" width=\"660\" height=\"76\" rx=\"6\" fill=\"#161B22\" stroke=\"#30363D\"/>\n<text x=\"340\" y=\"162\" fill=\"#D29922\" font-size=\"11\" text-anchor=\"middle\" font-weight=\"600\">DLQ message must contain</text>\n<text x=\"20\" y=\"178\" fill=\"#8B949E\" font-size=\"9\">Raw payload bytes (exact) \u00b7 Exception class + stack trace per attempt \u00b7 Timestamp per attempt</text>\n<text x=\"20\" y=\"193\" fill=\"#8B949E\" font-size=\"9\">Consumer version + instance ID \u00b7 Original topic/partition/offset (Kafka) or queue URL/msgId (SQS) \u00b7 Attempt count</text>\n<text x=\"20\" y=\"208\" fill=\"#3FB950\" font-size=\"9\">Distinguish: PERMANENT (same exception every time) vs TRANSIENT (different exceptions, resolves during backoff)</text>\n<text x=\"20\" y=\"216\" fill=\"#8B949E\" font-size=\"8\">Store DLQ in queryable format (Elasticsearch / S3+Athena). Test: synthetic poison pill in CI + chaos flag in staging.</text>\n<defs>\n<marker id=\"pp1\" markerWidth=\"6\" markerHeight=\"5\" refX=\"6\" refY=\"2.5\" orient=\"auto\"><polygon points=\"0 0,6 2.5,0 5\" fill=\"#2F81F7\"/></marker>\n<marker id=\"pp2\" markerWidth=\"6\" markerHeight=\"5\" refX=\"6\" refY=\"2.5\" orient=\"auto\"><polygon points=\"0 0,6 2.5,0 5\" fill=\"#D29922\"/></marker>\n<marker id=\"pp3\" markerWidth=\"6\" markerHeight=\"5\" refX=\"6\" refY=\"2.5\" orient=\"auto\"><polygon points=\"0 0,6 2.5,0 5\" fill=\"#BC8CFF\"/></marker>\n<marker id=\"pp4\" markerWidth=\"6\" markerHeight=\"5\" refX=\"6\" refY=\"2.5\" orient=\"auto\"><polygon points=\"0 0,6 2.5,0 5\" fill=\"#3FB950\"/></marker>\n<marker id=\"pp5\" markerWidth=\"6\" markerHeight=\"5\" refX=\"6\" refY=\"2.5\" orient=\"auto\"><polygon points=\"0 0,6 2.5,0 5\" fill=\"#F85149\"/></marker>\n</defs>\n</svg>"}]};
var NC=new Set(['backpressure','schema','observability','async','realworld']);
var ac='all',oc={},fc={},rv={},hr=false,ho=false;
function bsb(){
var cats=Object.keys(CM);
var h='<span class="sbl">Filter</span>';
h+='<button class="sbi active" onclick="sc(\'all\',this)" id="sb-all"><span class="sdot" style="background:#8B949E"></span>All<span class="scnt">'+QS.length+'</span></button>';
h+='<span class="sbl">Category</span>';
cats.forEach(function(c){
var m=CM[c],n=QS.filter(function(q){return q.cat===c;}).length;
h+='<button class="sbi" onclick="sc(\''+c+'\',this)" id="sb-'+c+'"><span class="sdot" style="background:'+m.dot+'"></span>'+m.label+(m.isNew?' <span style="font-size:9px;color:#BC8CFF">new</span>':'')+'<span class="scnt">'+n+'</span></button>';
});
document.getElementById('sb').innerHTML=h;
}
function sc(c,el){
ac=c;
document.querySelectorAll('.sbi').forEach(function(b){b.classList.remove('active');});
(el||document.getElementById('sb-'+c)).classList.add('active');
rl();
}
function thr(){hr=!hr;document.getElementById('btn-hr').classList.toggle('on',hr);rl();}
function tho(){ho=!ho;document.getElementById('btn-ho').classList.toggle('on',ho);rl();}
function ea(){gf().forEach(function(q){oc[q.id]=true;rv[q.id]=true;});up();rl();}
function ca(){oc={};rl();}
function tc(id){
if(oc[id])delete oc[id]; else{oc[id]=true;rv[id]=true;}
up();rl();
}
function tf(cid,fi){
var k=cid+'-'+fi;
if(fc[k])delete fc[k]; else fc[k]=true;
rl();
}
function mr(id,e){e.stopPropagation();rv[id]=true;up();rl();}
function up(){
var t=QS.length,d=Object.keys(rv).length,p=Math.round(d/t*100);
document.getElementById('pf').style.width=p+'%';
document.getElementById('pc').textContent=d+' / '+t;
document.getElementById('s-rev').textContent=d;
}
function gf(){
var q=document.getElementById('srch').value.toLowerCase();
return QS.filter(function(item){
if(ac!=='all'&&item.cat!==ac)return false;
if(hr&&rv[item.id])return false;
if(ho&&item.diff!=='hard')return false;
if(q&&!item.title.toLowerCase().includes(q)&&!CM[item.cat].label.toLowerCase().includes(q))return false;
return true;
});
}
function rl(){
var items=gf();
document.getElementById('s-show').textContent=items.length;
document.getElementById('s-hard').textContent=QS.filter(function(q){return q.diff==='hard';}).length;
if(!items.length){
document.getElementById('ql').innerHTML='<div class="empty"><div class="empty-n">⊘</div><div class="empty-t">No questions match</div><div>Try a different filter or clear the search</div></div>';
return;
}
var h='';
items.forEach(function(q){
var io=!!oc[q.id],ir=!!rv[q.id];
var cl=CM[q.cat].label,dl=q.diff==='hard'?'Hard':'Medium';
h+='<div class="qc'+(ir?' rev':'')+'" id="qc-'+q.id+'">';
h+='<div class="qh" onclick="tc('+q.id+')">';
h+='<div class="qn">Q'+String(q.id).padStart(2,'0')+'</div>';
h+='<div class="qcnt"><div class="qt">'+q.title+'</div>';
h+='<div class="qtags"><span class="tag t-'+q.cat+'">'+cl+'</span><span class="tag t-'+q.diff+'">'+dl+'</span>';
if(ir)h+='<span class="tag t-rev">Reviewed</span>';
if(NC.has(q.cat))h+='<span class="tag t-new">new</span>';
h+='</div></div>';
h+='<span class="qchev">'+(io?'▲':'▼')+'</span>';
h+='</div>';
if(io){
h+='<div class="qb">';
h+='<div class="blbl">Strong-hire answer</div><div class="abx">'+q.answer+'</div>';
h+='<div class="blbl">Trade-off to deliver proactively</div><div class="tbx">'+q.tradeoff+'</div>';
h+='<div class="blbl">Expected follow-ups — click to expand answer</div>';
h+='<div class="fql">';
(q.fq||[]).forEach(function(fq,i){
var k=q.id+'-'+i,io2=!!fc[k];
var fa=q.fqa&&q.fqa[i]?q.fqa[i]:'';
h+='<div class="fqi">';
h+='<div class="fqh" onclick="tf('+q.id+','+i+')">';
h+='<span class="fqa-arr">→</span><span class="fqq">'+fq+'</span>';
h+='<span class="fqchev" style="'+(io2?'transform:rotate(90deg)':'')+'">►</span>';
h+='</div>';
if(io2&&fa)h+='<div class="fqans">'+fa+'</div>';
h+='</div>';
});
h+='</div>';
var diags=(DG[q.cat]||[]);
if(diags.length){
h+='<div class="blbl">Visual diagrams</div><div class="diag-grid">';
diags.forEach(function(d){
h+='<div class="diag-card"><div class="diag-title"><span class="diag-name">'+d.title+'</span><span class="diag-hint">Click to enlarge</span></div><div class="diag-svg">'+d.svg+'</div></div>';
});
h+='</div>';
}
h+='<div class="bact">';
if(!ir)h+='<button class="abtn abtn-g" onclick="mr('+q.id+',event)">✓ Mark reviewed</button>';
h+='<button class="abtn abtn-n" onclick="tc('+q.id+')">↑ Collapse</button>';
h+='</div></div>';
}
h+='</div>';
});
document.getElementById('ql').innerHTML=h;
bindDiagCards();
}
var lbZ=1,lbPx=0,lbPy=0,lbOpen=false;
function applyLb(){
var el=document.getElementById('diag-lb-inner');
if(el) el.style.transform='translate('+lbPx+'px,'+lbPy+'px) scale('+lbZ+')';
}
function setLbZoom(z,reset){
lbZ=Math.min(3,Math.max(0.4,z));
if(reset){lbPx=0;lbPy=0;}
applyLb();
}
function resetLb(){setLbZoom(1,true);}
function openDiagLb(title,svg){
document.getElementById('diag-lb-title').textContent=title;
document.getElementById('diag-lb-inner').innerHTML=svg;
document.getElementById('diag-lb').classList.add('open');
document.getElementById('diag-lb').setAttribute('aria-hidden','false');
document.body.style.overflow='hidden';
lbOpen=true;
setLbZoom(1.35,true);
}
function closeDiagLb(){
document.getElementById('diag-lb').classList.remove('open');
document.getElementById('diag-lb').setAttribute('aria-hidden','true');
document.body.style.overflow='';
lbOpen=false;
document.getElementById('diag-lb-inner').innerHTML='';
}
function bindDiagCards(){
document.querySelectorAll('.diag-card').forEach(function(card){
if(card.dataset.bound) return;
card.dataset.bound='1';
card.setAttribute('role','button');
card.setAttribute('tabindex','0');
card.addEventListener('click',function(){
var name=card.querySelector('.diag-name');
var svg=card.querySelector('.diag-svg');
if(svg) openDiagLb(name?name.textContent:'Diagram',svg.innerHTML);
});
card.addEventListener('keydown',function(e){
if(e.key==='Enter'||e.key===' '){e.preventDefault();card.click();}
});
});
}
function setupLb(){
document.getElementById('diag-lb-close').onclick=closeDiagLb;
document.querySelector('.diag-lb-backdrop').onclick=closeDiagLb;
document.querySelectorAll('[data-lb-zoom]').forEach(function(btn){
btn.onclick=function(){
var d=btn.getAttribute('data-lb-zoom');
if(d==='+') setLbZoom(lbZ+0.2);
else if(d==='-') setLbZoom(lbZ-0.2);
else resetLb();
};
});
var vp=document.getElementById('diag-lb-vp');
var drag=false,sx,sy,px,py;
vp.addEventListener('wheel',function(e){
e.preventDefault();
setLbZoom(lbZ+(e.deltaY<0?0.12:-0.12));
},{passive:false});
vp.addEventListener('mousedown',function(e){
if(e.button!==0) return;
drag=true;sx=e.clientX;sy=e.clientY;px=lbPx;py=lbPy;
vp.classList.add('dragging');
});
window.addEventListener('mousemove',function(e){
if(!drag) return;
lbPx=px+(e.clientX-sx);lbPy=py+(e.clientY-sy);
applyLb();
});
window.addEventListener('mouseup',function(){drag=false;vp.classList.remove('dragging');});
vp.addEventListener('dblclick',resetLb);
}
document.getElementById('srch').addEventListener('input',rl);
document.addEventListener('keydown',function(e){
if(e.target.tagName==='INPUT')return;
if(e.key==='/'||e.key==='f'){e.preventDefault();document.getElementById('srch').focus();}
if(e.key==='Escape'){if(lbOpen){closeDiagLb();return;}document.getElementById('srch').value='';rl();}
if(lbOpen&&(e.key==='+'||e.key==='=')){e.preventDefault();setLbZoom(lbZ+0.2);}
if(lbOpen&&e.key==='-'){e.preventDefault();setLbZoom(lbZ-0.2);}
if(e.key==='h'||e.key==='H')tho();
if(e.key==='r'||e.key==='R')thr();
});
setupLb();bsb();rl();
</script>
</body>
</html>