Day 93 of 100·System Design Day·716 words

Synthesis — design Traverse from scratch, cold

The second mock: a latency-sensitive ingest + scheduling product, designed in 40 minutes.

Concept → your projects

Timer on. Talk out loud. Then compare.

The prompt

"Design a system that passively captures a developer's problem-solving activity from coding platforms (via a browser extension and a mobile app), schedules spaced-repetition revisions of what they solved, and notifies them — with analytics. Users: ~100k eventually, latency-sensitive writes."

The model answer

1. Requirements (2 min). FRs: capture submissions from platforms; store attempts/code; schedule revisions per problem; notify (push + in-app); analytics/streaks. NFRs: write latency p95 < 300 ms (the capture path is on the user's critical path — every submission they make); reads tolerate 1–10 min staleness; delivery is best-effort (push) with durable in-app truth; durability of user history is critical (irreplaceable).

2. Capacity (3 min). 100k users × 20 submissions/day ≈ 23 writes/s average, ~70/s peak. Reads: dashboard polling ~1 per user-minute ≈ 1.6k/s at 100k users — reads dominate → cache + indexes. Revision scheduling: 100k users × 50 due-reviews/day ≈ 60/s jobs. Storage: ~1 KB/submission + code attempts (bounded) ≈ tens of GB/year → Postgres fine, partition submissions by month later. Conclusion: the write path is latency-sensitive, the read path is cacheable, and jobs are modest — that shapes everything.

3. The critical decision: get side effects off the write path (5 min). The capture endpoint does: auth → device check → idempotency dedup → find-or-create problem → insert submission → XP/solve (fast, transactional) → enqueue derived work (AI analysis, FSRS scheduling, streaks, achievements, notifications) → respond 202. Quantify the win: synchronous AI (multi-provider LLM ladder, 15 s timeouts) made p95 ~10 s; async made it ~200 ms — the 50× story is a latency-budget decision, not a throughput one.

4. Data model (5 min). users, devices (revocable), problems (unique platform+slug), submissions (unique userId+idempotencyKey), solves (unique userId+problemId = the fact), revisions (scheduled, with prediction columns for calibration), revision_attempts (observed), streak_days (PK userId+date), ring_days (with goal snapshots), friendships (double-row), notifications + notification_deliveries (intent/outcome ledgers), push_tokens. Constraints everywhere as the dedup layer.

5. Async architecture (5 min). A Redis-backed job queue (BullMQ): three queues (submission-processing, ai-analysis, notification-delivery), concurrency caps and rate limits per queue, retries with exponential backoff, idempotent handlers (upserts, conditional updates, P2002 catches), stalled-job recovery. Notification delivery as a Postgres outbox with a lease-claim drain (locks never cross network I/O — Day 62) so pushes survive Redis restarts; BullMQ jobs are triggers, the DB rows are the state.

6. Scheduling (4 min). FSRS-style: per-problem/user memory state (stability, difficulty), due dates computed from retrievability targets; predictions persisted with a model version; same-day and leech handling; workload shaping. Evaluation built in: predicted vs observed, bucketed by model version — the calibration harness.

7. Reads & caching (3 min). Cache-aside with per-data TTLs (120–600 s), explicit invalidation from workers after derived writes, keys namespaced entity:id:facet for surgical invalidation, timeouts (2 s) and degrade-to-no-op so Redis is never a dependency. Stats are cached projections; schedules are read from the DB (never cached — they drive actions).

8. Notifications (3 min). In-app truth (rows) + push as a best-effort channel via APNs/FCM (the OS holds the socket — no WebSocket fleet), coalescing at data and display layers, quiet hours by user timezone, terminal statuses for unconfigured/invalid tokens.

9. Failure drill (4 min). Redis down → reads degrade to DB, enqueue fails → request retries idempotently → known gap: derived work can be stranded → fix with a pending flag + reconciliation sweep (or a submission outbox). Worker crash → redelivery + idempotent handlers. APNs outage → retries with backoff, terminal after 5. Queue loss → AOF + DB truth + recomputable derived state.

10. What I'd add next (2 min). Submission outbox (close the enqueue gap), Redis split (cache vs queue policies), rate limiting in Redis, cron job claiming for multi-replica, SLOs on capture latency, and a real control group for scheduling experiments.

Self-grading rubric

Did you put the latency budget at the center? Did you separate facts (submissions/solves) from derived (revisions/streaks) and say which is recomputable? Did you justify BullMQ over RabbitMQ/Kafka (Day 59)? Did you keep notifications best-effort with durable truth? Did you name the enqueue gap yourself? Five yeses = ready.

Bulletproof your resume

Say these answers aloud before moving on

These are the follow-ups an interviewer will actually ask about your projects.

01

What's the core insight of this design?

Separate irreplaceable facts from recomputable derivations, and let the derivations be eventually correct. Submissions and solves are facts — they must be durable and fast to write. Revisions, streaks, rings, achievements, and notifications are derivations of those facts — they can lag by seconds and, if lost, be recomputed by replaying facts. That's what makes the 50× latency win safe: I moved everything recomputable off the request path, so the write path only does what must be synchronous.

02

Why is the read path the harder problem at 100k users?

Because read volume dominates (dashboard polling ~1.6k rps vs ~70 writes/s) and reads fan out per user across stats, schedules, rings, and feeds. The answers are indexes + cache-aside with per-data TTLs + explicit invalidation from workers, and — the part people miss — making the schedule reads uncacheable (they drive actions) while caching only its projections. If reads still hurt at 10×, the ladder is read replicas for stats and bucketed rollup tables before anything exotic.

03

Where would Kafka finally make sense here?

If the submission stream became the product's center of gravity: analytics over the full event history (funnels, cohort retention on raw events), multiple independent consumers (ML training, analytics, notifications) each with their own offsets, and throughput past ~100k/s. Note the shape: the submission table already is an event log — the graduation is CDC off it into a log, not a redesign. Until then, a Redis-backed work queue plus Postgres truth is the right-sized answer.