# Like-counter system design board: panel review

Artifact v1: `like-counter-system-design.html` / v2: `like-counter-system-design-v2.html`
Panel: senior-level reviewer (mechanism depth), staff-level reviewer (judgment), adversarial fact-checker (truth only, v1 round).

---

# V4 REVIEW (2026-08-07): 4.7 / 5 - strong hire at senior, hire-to-strong at staff

Assessed by line diff against v3 plus an adversarial pass on every new mechanism (each prior round's fixes shipped a composition bug, so the new compositions got special attention). Every open item from the v2/v3 fix list is now genuinely closed:

| Open item | v4 resolution | Verified |
|---|---|---|
| BLOCKER: 63/64 undercount (guard vs bucketed keying) | Explicit two-stage topology: stage 1 dedupe keyed item#hash(user)%64, then a stated keyBy(item) shuffle to one window subtask emitting ONE (item, window, +N)/s. The board even explains the 63/64 failure the missing stage would cause, and works the one-second viral trace end to end. | yes |
| Sink atomicity + applied_windows growth | Both killed by one change: fence on the counter row itself (ADD count :n SET last_window=:w CONDITION last_window < :w). Guard and increment are one atomic update; the side table is deleted from the schema and the SVG. | yes - zero applied_windows references remain |
| Pin-move split-brain | Epoch-fenced pin: epoch increments before the new home serves, stragglers' conditional writes fail closed; hard-loss tail becomes a reconciliation delta, never a merge; far-user write hop priced; on-call signal stated ("old-home rejection spike first"). | yes |
| Reconciliation implementability | PITR snapshot export at position T, wait for sink watermark to pass T, correction enters the stage-2 keyed stream as its own window so the row fence serializes it with live traffic. | yes |
| Cost 2x undercount | "each action is two writes... ~$400K/month... at $2.5-5M/year the number does real work: it pressures build-vs-buy, and self-hosted ScyllaDB is the runner-up this bill has to beat." | yes |
| 1K vs 5-10K breakpoint contradiction | Simpler design now breaks at "the ~1K writes/sec partition cap... the first modestly viral event, not the mega-viral one." | yes |
| "exact" API label | Now "read-your-writes for the actor, else eventual." | yes |
| Invented Kafka config | enable.idempotence=true with 5 in flight; Streams 24h retention named as the tailer-downtime bound. | yes |
| Erasure product question | Requirements now ask whether celebrity deletion visibly draining a public count is acceptable. | yes |
| OPERATIONS (skipped by v1-v3) | New Deep Dive G: SLOs (staleness measured as watermark lag, "not inferred"), alerts mapped one-to-one to SLOs, backpressure philosophy (fail writes fast, let counts go visibly stale, overlay keeps actors correct), the dedupe-regime cutover (fence initialization at a marked stream position, rollback symmetric), runbook heads. | yes |

Also new and good: auth note (userId from session, not body), gateway "abuse hook" in the diagram, "pin move with the epoch increment as step one" as a runbook head.

## Residual findings (both minor, both one-line fixes - the remaining live-probe surface)

1. Sharded-counter x fence routing: item_count_shard rows each carry last_window, but nothing states the window -> shard mapping. If a hot item's window write picked a shard randomly, a replayed window could land on a different shard and pass that shard's fence (double-apply). Needs one sentence: deterministic routing, e.g. shard = window_id % K.
2. Where does the epoch live? A DynamoDB CONDITION evaluates against the item being written, so "CONDITION epoch = :current" implies either an epoch attribute on every edge row (bump-on-first-write migration unstated) or an out-of-band range-lease check before the write (which must not reintroduce check-then-act). The fencing-token shape is correct; its storage location is unstated.

## Scorecard trajectory

| Dimension | Wt | v1 | v2 | v4 |
|---|---|---|---|---|
| Requirements and estimation | 10% | 3.0 | 4.5 | 5.0 |
| API design | 10% | 3.0 | 4.5 | 5.0 |
| Data model | 10% | 4.0 | 4.5 | 4.5 |
| Core architecture | 15% | 4.0 | 4.5 | 5.0 |
| Correctness guarantees | 20% | 2.0 | 3.0 | 4.5 |
| Scale and hot items | 10% | 2.5 | 4.0 | 4.5 |
| Failure and recovery | 10% | 3.0 | 4.0 | 4.5 |
| Ops, cost, evolution | 10% | 2.0 | 3.0 | 4.5 |
| Communication / artifact | 5% | 3.5 | 4.5 | 5.0 |
| **Weighted** | | **3.0** | **4.0** | **4.7** |

## Verdict

Strong hire at senior; at staff, hire with a genuine case for strong hire - the two dimensions that held v2 at "hire" (operations and unexamined commitment mechanics) are now the two strongest additions. The remaining probe surface is the two residuals above plus one delivery risk: the artifact is now so complete that the interview skill shifts from content to restraint - present steps 1-4 in ~20 minutes and let the interviewer pick the deep dives, rather than presenting all of A-G unprompted.

---

# V3 REVIEW (2026-08-07, delivery-order restructure): still 4.0 / 5

V3 (`like-counter-system-design-v3-hi-order_1.html`) reshapes v2 into interview delivery order: Requirements -> Core Entities -> API -> High-Level Design -> Deep Dives A-F, with per-step time budgets and "let the interviewer steer." Assessed by direct diff against the v2 panel findings (content is v2 compressed; no new mechanisms introduced, so no fresh agent round).

## What v3 improves

- The delivery format itself. This fully answers the v1 staff critique "organized to look complete, not to drive a 45-minute conversation": entities stay shallow with schema deferred, the high-level walkthrough is one paragraph with every clause mapped to a deep dive, probes are at the end as worked traces, and an out-of-scope list exists. As a script for the actual interview, v3 is the right artifact.
- Dropped the invented Kafka config ("max.in.flight=1 per key"); now states the requirement (idempotent, order-preserving per key) without a fake mechanism. That was v2 finding NEW-6: fixed.
- Dropped the wrong warrant on display dips (v2 N2); now purely an assumption question.
- "userId for mutations comes from the auth session, not the body" is new and good.

## What v3 does NOT fix - the v2 fix list is almost entirely unapplied

| v2 finding | Status in v3 |
|---|---|
| BLOCKER: applied_windows keyed (item, window) while the stream is keyed item#hash(user)%64 -> up to 63/64 of a viral item's partial windows rejected as replays | **STILL OPEN.** Same schema line, same PUT-IF wording. The walkthrough says "batches them per item per second," which would require a second-stage re-key by item after the per-edge dedupe, but no re-key stage is ever stated. Either state the two-stage topology or fence on the counter row. |
| Guard write / counter write not atomic (needs TransactWriteItems or counter-row fence) | STILL OPEN, same wording |
| applied_windows has no TTL (unbounded, ~10^9 rows/day) | STILL OPEN, schema unchanged |
| Home-region pin failover fencing (LWW split-brain during pin move) | STILL OPEN, paragraph unchanged |
| Reconciliation: recount needs snapshot/PITR, counter(T) needs sink watermark, fence undefined | STILL OPEN, same sentence |
| Cost line undercounts own write amplification ~2x (idempotency writes) | STILL OPEN, same $200K line |
| Simpler-design breakpoint (5-10K/s/row) contradicts own 1K WCU cap | STILL OPEN, both numbers still present |
| GET /likes/{userId} labeled "exact" vs overlay + eventually consistent serving path | STILL OPEN |
| Ops dimension (SLOs, backpressure, cutover, on-call) | STILL ABSENT |

## Verdict

Score stays 4.0 / 5: communication moves to full marks, correctness does not move because the blocker and both significant sink findings survive verbatim. v3 is the right SHAPE; apply the v2 fix list inside it (mostly one-line edits: per-bucket guard key or counter-row fence + stated re-key stage; TransactWriteItems or the same counter-row fence; TTL on applied_windows; a fencing sentence for the pin move; snapshot + watermark words in reconciliation; 2x the cost line; reconcile the 1K/5-10K numbers; drop "exact" or state the strong-read fallback). The senior's standing live probe (one second of the viral item end to end through the sink) remains the exact question this artifact cannot yet survive.

---

# V2 RE-REVIEW (2026-08-07): 4.0 / 5 - hire at senior, hire at staff

Up from 3.0. Both reviewers re-verified their own v1 findings item by item, then hunted for errors the rewrite introduced.

| Reviewer | v1 verdict | v2 verdict |
|---|---|---|
| Senior | Lean hire | **Hire** - 8 of 12 findings fixed, 4 partial, 0 unfixed; "every remaining defect is one-line-fixable and none reflects a conceptual misunderstanding" |
| Staff | No hire | **Hire** (not strong) - 6 of 9 fixed; "frames and trades off at staff level but has not yet demonstrated they can own this system in production" |

## v2 scorecard

| Dimension | Wt | v1 | v2 | Movement |
|---|---|---|---|---|
| Requirements and estimation | 10% | 3.0 | 4.5 | Interrogation with stated assumptions and design consequences; peak range and window numbers corrected |
| API design | 10% | 3.0 | 4.5 | Idempotency key actually implemented (lookup/replay/store, both mutations); pagination; batch feed endpoint |
| Data model | 10% | 4.0 | 4.5 | Bucketed partition key; idempotency table; hard-delete erasure; applied_windows lacks a TTL |
| Core architecture | 15% | 4.0 | 4.5 | Stores committed with reasons (Dynamo Streams vs plain Cassandra rejected correctly); Redis role resolved |
| Correctness guarantees | 20% | 2.0 | 3.0 | Concepts now right (sink idempotence, deterministic keying, correction deltas) but ONE NEW BLOCKER shipped, see below |
| Scale and hot items | 10% | 2.5 | 4.0 | Defense 0 added at the truth store; defenses re-argued with correct rationales |
| Failure and recovery | 10% | 3.0 | 4.0 | Correct crash trace at concept level; bounded 7-day replay rebuild; pin-failover mechanics missing |
| Ops, cost, evolution | 10% | 2.0 | 3.0 | Cost line exists but miscounts its own design; ops remains the one unfixed staff dimension |
| Communication / artifact | 5% | 3.5 | 4.5 | Coaching tone gone; open-questions section is the strongest addition; some review-reactive framing residue |

## THE NEW BLOCKER (senior, NEW-1)

**The two flagship v1 fixes interact to create a worse bug than either original.** The Kafka/Flink key is now `item#hash(user)%64` (fix for salting), so a viral item's windows are computed by up to 64 independent subtasks, each flushing its own partial `+N` per second. But the sink guard is keyed `applied_windows(item_id # window_id)` (fix for double-apply). First bucket's partial marks the window applied; the other 63 partials are rejected as replays. **~63/64 of a viral item's likes are silently dropped - on exactly the case the design centers on.** One-line fix: guard key `item # bucket # window`, or put the fence on the counter row itself. Also invalidates Defense 1's "one sink write per item per second."

## Other new findings (both reviewers, convergent where noted)

1. SIGNIFICANT - Guard write and counter write are separate tables with no TransactWriteItems: crash between them either loses a window or double-applies it. The board's own maxim "never check-then-act in the application" is violated by its own sink. Cleaner fix the board missed: fence on the counter row (`ADD count :n SET last_window=:w CONDITION last_window < :w`), atomic, no extra table.
2. SIGNIFICANT - `applied_windows` growth is unbounded (order 10^9 rows/day); every other state got a retention story, this one got none. Needs TTL >= replay horizon (~7-8 days). The v1 unbounded-state bug, re-introduced one hop downstream.
3. SIGNIFICANT (convergent) - Home-region pin failover can recreate the exact LWW split-brain the board rejects active-active for: the new home evaluates conditional writes against an async replica missing the old home's unreplicated tail; op_ids fork. No fencing story, no bound on "brief write pause", no cross-region read-your-writes answer, unpriced far-user write latency.
4. PARTIAL from v1 (convergent) - Reconciliation: correction-delta direction is right, but `recount at stream position T` is not implementable as a live table scan (needs PITR/snapshot export), `counter(T)` requires waiting for the sink watermark to pass T, and "v-compatible" is undefined (version-equality fencing never passes on a hot item).
5. MEDIUM (convergent) - The cost line undercounts the design's own write amplification ~2x (idempotency-table write per action ignored; also on-demand prices halved in Nov 2024, errors partially cancel). At ~$2.4-5M/yr it should pressure build-vs-buy (self-hosted Scylla runner-up) and drives no decision.
6. MEDIUM (convergent) - Simpler-design breakpoint self-contradiction: section 10 says per-event ADD breaks at ~5-10K/s on one row; section 8 says a Dynamo partition key caps near 1K/s. By the board's own numbers the simple design breaks 5-10x earlier than claimed.
7. MEDIUM - "Rounded display absorbs dips" fails on small counts (5 -> 4 is fully visible; nothing rounds at 3). Right assumption, wrong warrant.
8. MEDIUM - Additive-migration claim is rosy: cutover from per-event op_id-token ADDs to windowed sink writes must switch dedupe regimes on a live counter without double-applying, glossed in one clause.
9. MINOR - "acks=all, max.in.flight=1 per key semantics" is not a real Kafka config (it is per-connection; the right answer is enable.idempotence=true, ordering preserved with up to 5 in flight). Also unmentioned: Dynamo Streams 24h retention bounds tailer downtime.
10. MINOR - `GET /likes/{userId}` still annotated "exact" while served by overlay + eventually consistent read; label-vs-mechanism drift the v1 review dinged elsewhere.
11. MINOR - Erasure decrementing public counts (celebrity deletion drains 50M visible likes) is a product decision committed silently; belonged in the section 0 question list.
12. NOT FIXED - Operations remains one metric deep: no SLOs, no staleness alerting, no backpressure/degradation story at 580K/s, no deployment/cutover or on-call/runbook narrative.

## What v2 got right that v1 got wrong (verified, not just claimed)

Bucketed edge-store key with correct math (16.7K/64 = 260/s) and the recount fan-out cost acknowledged; deterministic Kafka keying with the double-count failure correctly explained; DynamoDB committed with `if_not_exists(op_id,0)+1` inside the conditional UpdateItem (no external sequencer, no-op emits no stream record); sink idempotence correctly located at the output, with a correct Flink crash trace; RocksDB dedupe TTL'd to the replay horizon with reconciliation named as backstop; Redis strictly read-through; stampede handled (SWR + coalescing); erasure hard-deletes with 7d retention < 30d window so rebuilds cannot re-materialize erased data; requirements interrogated; simpler design steelmanned to "this is where I would start"; open-questions section with genuine self-doubt.

## To reach 4.5+

1. Fix the blocker: per-bucket guard key or counter-row fence (also restores Defense 1's claim).
2. Make the sink atomic: TransactWriteItems or the counter-row fence.
3. TTL applied_windows to the replay horizon.
4. Fence the pin move (old-home write rejection, bounded pause, where proxied writes go) or scope the design to single-region-of-record explicitly.
5. Reconciliation: snapshot/PITR export for the recount, watermark wait for counter(T), define the fence.
6. Fix the cost line (2x) and the 1K-vs-5-10K breakpoint contradiction.
7. Add the ops panel: SLOs, staleness alert, backpressure behavior, cutover story.

## The two live probes still standing

1. (Senior) "Walk me through one second of the viral item end to end: how many Flink subtasks hold pieces of that item's window, how many sink writes does that (item, window) produce, what is the applied_windows key for each - then crash the sink between guard write and counter write and tell me the count."
2. (Staff) "Your home region for a hot item range goes dark mid-partition with proxied writes in flight. Move the pin: how do you fence the old home so you don't get the LWW split-brain you rejected, how long is 'brief' really, and what does on-call see first?"

---

# V1 REVIEW (original, unchanged below)

## Committee verdict: 3.0 / 5 - lean hire at senior, no-hire at staff

The architecture backbone is genuinely good, but three of the board's headline guarantees are false as written, and confident-but-wrong is worse in an interview than absent.

| Reviewer | Verdict |
|---|---|
| Senior (mechanisms) | Lean hire: right backbone, four mechanism failures on contact with the named systems; live-probe recovery decides it |
| Staff (judgment) | No hire at staff (hire, likely strong hire, at senior): nothing that distinguishes staff is present |
| Fact-checker | Skeleton sound, most numbers verified, three headline guarantees false as stated |

## Scorecard

| Dimension | Wt | Score | Notes |
|---|---|---|---|
| Requirements and estimation | 10% | 3.0 | Numbers clean, but every requirement self-granted; two internal number inconsistencies |
| API design | 10% | 3.0 | 200-on-noop right; Idempotency-Key decorative; retry-after-unlike hole; no who-liked pagination |
| Data model | 10% | 4.0 | Edge-table uniqueness insight is the strongest thing on the board; op_id allocation unexplained; GDPR retention unaddressed |
| Core architecture | 15% | 4.0 | Truth/derived split, CDC-not-dual-write; store picks not actually interchangeable; Redis role self-contradictory |
| Correctness guarantees | 20% | 2.0 | Salting breaks dedupe (certain); sink double-apply (certain); reconciliation race; "safety holds at every layer independently" false |
| Scale and hot items | 10% | 2.5 | Windowed agg right; source-of-truth hot partition missed entirely; Defense 2 rationale contradicts Defense 1 |
| Failure and recovery | 10% | 3.0 | Good enumeration habit; crash story wrong at the sink; Kafka replay assumes unbounded retention |
| Ops, cost, evolution | 10% | 2.0 | One metric total; no cost model; multi-region hand-wave breaks own "strong per row" |
| Communication / artifact | 5% | 3.5 | Clean board; coaching tone is a liability; half the canvas empty |

Weighted: ~3.0 / 5.

## Blockers (both "certain", each found independently by two reviewers)

### 1. `rand()` partition salting breaks your own dedupe
Defense 3 salts with `item_id + rand()%K`, but the op_id dedupe state is local RocksDB keyed by partition. Concrete interleaving:
1. CDC emits like(item X, u1, op7), salt=2 -> partition P2 -> instance B applies +1, records last(X,u1)=7 in B's RocksDB.
2. CDC tailer crashes before committing its offset; restarts; re-emits the same change, salt=0 -> partition P0 -> instance A. A has never seen (X,u1); op7 > nothing -> +1 applied AGAIN.
Permanent double count, on exactly the viral items salting targets. Section 11's "the partition keeps them ordered" is false for any salted item. "Deltas commute" is irrelevant: commutativity is not idempotence.
Fix: deterministic salt `hash(user_id) % K` (spreads the item, keeps each (item,user) on one partition, preserves per-edge ordering), or an explicit re-key-by-(item,user) shuffle before dedupe.

### 2. The source of truth melts before anything you defended
Edge store is partitioned by item_id ("sharding key: item_id everywhere" presented as a virtue). Your own viral item, 1M likes/min ~= 17K conditional writes/sec, hits ONE partition: DynamoDB caps a single key near 1K WCU/s (adaptive capacity cannot split one key value); Cassandra LWT serializes per-partition Paxos. All three hot-item defenses protect Kafka and the counter row; none protects the edge store, which dies first.
Fix: bucketed partition key `(item_id, hash(user_id) % N)`, and acknowledge it reshapes the reconciliation scan.

## Significant findings

### 3. "Effectively-once ... never wrong-by-duplication" is unearned at the sink
op_id dedupe protects the processor's input; nothing protects its output. Flink restores RocksDB from the same checkpoint as the offsets, so after a crash the replayed events are re-accepted (the dedupe drops nothing, contrary to section 9), and the +N window increments already INCRBY'd to the counter store apply a second time. The signature phrase "offsets plus per-edge op_id dedupe give effectively-once application" attributes the guarantee to the wrong mechanism.
Fix: idempotent sink (conditional insert keyed on `(item_id, window_id)`) or transactional sink; say which.

### 4. The Idempotency-Key header is decorative
The conditional write branches on row STATE only; the key is never stored, compared, or mentioned again; DELETE carries no key at all. And state-dedupe is not key-dedupe:
1. Like -> POST(key=A) -> row LIKED(op7); 200 lost in the network.
2. User unlikes -> row UNLIKED(op8).
3. Transport retries POST(key=A) -> state is UNLIKED -> treated as a REAL transition -> LIKED(op9), +1.
User chose unliked; system shows liked and counted it. "Retries safe end to end" is false as an absolute. Fix: implement the key (store it, replay the stored response) or delete it from the API and state the actual guarantee.

### 5. Reconciliation is the design's best corruption vector
"Recount edges -> overwrite the counter": the recount reflects time T, the overwrite lands at T+delta and destroys every delta applied during the scan - worst on hot items, which the job explicitly prioritizes. Needs a watermark/op_id fence, a conditional overwrite against a counter version, or route the correction through the pipeline as a delta. Both senior and staff called this the single best live probe against this board. Related: nothing prevents a rebuilt counter from going negative; no clamp/alert.

### 6. Staff-layer absences (why the staff verdict is no-hire)
- Zero requirement interrogation. Missing openers: is globally reconciled unlike a real requirement? must the count be monotonic to a viewer (shard-sum + mixed caches can render a count that goes backwards)? is the real problem counting or integrity (bot/purchased likes make exact counting pointless; only control on the board is "auth, rate limit")?
- GDPR/privacy: the edge table is behavioral PII retained forever (state LIKED|UNLIKED never deletes rows), also in Kafka retention and Flink state; Kafka-replay rebuild would re-materialize erased data.
- No cost model, and the load-bearing primitive is the expensive one: conditional writes are LWT/Paxos on Cassandra, priced per-request on DynamoDB; at the board's own 300-500K/s this is order $1M+/month on Dynamo on-demand before storage. No dollar or hardware figure anywhere.
- "Why not simpler" is a strawman: bare Redis INCR is the weakest alternative. The strongest simpler design is already on the board: edge store + async increment (op_id as dedupe token) or a materialized count column + the same reconciliation job. That deletes CDC, Kafka, and Flink at the stated 58K/s average. The board never locates the scale threshold where the streaming tier earns its place.
- Multi-region: "edge store replicates cross-region" hides active-active conditional-write conflicts; cross-region LWT is impractical and Dynamo global tables are last-writer-wins, which silently breaks section 10's "Strong per row."
- Celebrity deletion "through the same pipeline, no special path": 50M synthetic transitions for one item land in ONE Kafka partition (partitioned by item_id), head-of-line blocking that item's live traffic; no backpressure/priority story anywhere; and for account deletion, UNLIKED is not erasure (see GDPR).

### 7. Mechanism facts (senior + fact-checker)
- Cassandra CDC gives raw per-replica mutations: RF duplicate copies, no cross-replica ordering, and no before-image, so the UNLIKED->LIKED delta mapping cannot be derived from it. DynamoDB Streams (NEW_AND_OLD_IMAGES, ordered per key, once per change) or Scylla CDC tables actually deliver what the box claims. The three named stores are presented as interchangeable; they are not. Reordered CDC events + "op_id <= last" dedupe = dropped real transition = permanent undercount.
- op_id = next() is unexplained and is the linchpin: no ambient per-row sequence exists in these stores. Dynamo can do it atomically in one UpdateItem; Cassandra needs read-then-CAS at full Paxos cost, and "row absent OR state == UNLIKED" is two different statements (INSERT IF NOT EXISTS vs UPDATE IF), not one.
- Dedupe state at 10^11 edges is ~3-5TB of RocksDB keyed state: multi-TB checkpoints, restores in tens of minutes. Right answer: TTL the dedupe horizon to the replay window and name reconciliation as the backstop - the board has the pieces and never connects them ("Safety holds at every layer independently" is false once the TTL exists).
- Read-your-writes: 10-100x of 58K/s = 0.6-5.8M page renders/sec, each needing exact button state (x N items per feed page). As drawn these land on the truth store (with LWT, correct reads need SERIAL/quorum). The "(or a per-user session cache)" parenthetical does all the real work and is never designed.
- Redis role is self-contradictory: storage table says "Hot counters - Redis - atomic INCRBY/DECRBY"; read path says Redis is a read-through cache in front of the durable store. If the pipeline INCRBYs Redis AND writes the durable store, that is the dual-write the board disavows.
- Defense 2's "contention drops K-fold" contradicts Defense 1: after 1s windowing there is ~one write per item per second; there is no contention left to shed. (The storage-node-hotspot argument would survive, but it is not the argument made.)
- "Rebuild by replaying from Kafka" assumes unbounded retention (~10^11+ events); log compaction cannot help (keeps last event per key, useless for summing deltas). The edge-store recount is the real rebuild path; Kafka replay is only valid for a bounded window on top of a snapshot.
- CDC tailer -> Kafka needs an idempotent, order-preserving producer, or per-key reordering re-introduces the salting failure mode with no salting involved.
- Cache stampede: viral item + synchronized TTL expiry = thundering herd on count service; needs stale-while-revalidate or request coalescing. Long-tail CDN hit rate is Zipf-dependent; worth saying.

### 8. Internal number inconsistencies (fact-checker)
- Section 5 prose: "10,000 events in that window" vs the same section's viral item "1M likes / min" = ~16,667 events per 1s window. Off by 1.7x within one section.
- "Peak factor 5 to 10x -> plan for 300K to 500K": 58K x 5 = 290K, x 10 = 580K. Rounds one end up and the other down. Trivial, but it is the flagship say-it-out-loud math.
- Verified clean: 5B/86,400 ~= 58K/s; 10^11 edges ~= tens of TB with overhead+RF; the +1 mini-trace; 184,203 -> 184.2K; shard snapshot values contradict nothing.

### 9. The coaching tone is a liability on the artifact itself
"Say the phrase," "Interviewers listen for that distinction," "The follow-up they always ask," "Prep drill," "Close with this." An interviewer probes hardest exactly where phrasing sounds rehearsed, and the most rehearsed line on the board (effectively-once) is also the wrong one (finding 3). No "open questions," "what I'd cut," or "decision I'd revisit" section exists. Also: half the rendered canvas is empty dark space.

## What all three reviewers credited

- The central split, stated early and derived from requirements: strongly consistent tiny-scope truth (one row per edge), eventually consistent derived counter for display. The closing one-sentence summary is exactly right.
- Idempotency in the data model, not request dedupe - the load-bearing insight most candidates miss; like/unlike as state transitions with 200-on-noop.
- CDC over dual-write with the correct half-failure reason.
- Honest at-least-once / effectively-once framing (even though the mechanism attribution has the sink hole).
- Windowed aggregation as the primary hot-item lever; sharded counters with small K are textbook.
- Rebuildability as the recovery primitive; reconciliation doubling as backfill/bootstrap.
- "Do not COUNT(*) per request" called out; rounded display buying staleness tolerance.
- Freshness quantified end to end (CDC lag + window + TTL ~= 2-5s) and paired with a measurement.

## To raise the board a full point

1. Fix the two blockers: deterministic `hash(user_id)%K` salting; bucketed edge-store partition key. Both are one-line changes that show you saw the traps.
2. Replace absolutes with mechanisms: idempotent sink keyed on (item, window); fenced reconciliation overwrite (op_id high-water mark or counter-version CAS); implement the Idempotency-Key or delete it from the API.
3. Add the staff section: the simpler design (edge store + async increment + reconciliation) and the measured threshold where it breaks; a cost line for the conditional-write tier; three PM questions you would open with (abuse/integrity, GDPR erasure, monotonic display).
4. Commit to one store per guarantee instead of "Cassandra / Scylla / Dynamo" interchangeably; resolve Redis's role (cache XOR increment target).
5. Strip the coaching language into your own notes; add an "open questions / what I'd cut" panel.

## Live probes to practice (in order of damage)

1. "Your reconciliation job recounts and overwrites. On an item taking 100K likes/min where the scan takes ten minutes: what number is in the counter the second after the overwrite lands, what happened to the deltas that arrived during the scan, and how do you make the overwrite safe?"
2. "Defense 3 salts with rand()%K. Your dedupe state for (item,user) lives in one task's RocksDB. A CDC retry re-emits an event for that edge and draws a different salt. Trace it. What function do you salt with instead, and what invariant must it preserve?"
3. "The Flink job checkpoints at T, writes three +N window increments, crashes at T+30s. What does RocksDB restore to, which offsets replay, does op_id dedupe drop anything, and why doesn't the counter apply those three windows twice?"
4. "Your viral item takes 1M likes a minute. Walk me through the edge-store partition for that item_id: what does a conditional write cost there, and how do you re-shape the key without losing (item,user) uniqueness?"
5. "A like's 200 is lost, the user unlikes, the client retries the like with the same idempotency key. What state does the row end in, what did the user intend, and what does your header actually do?"
