Interviewer — Robinhood / stock trading (SDS)
Candidate prompt: prompt. Method: Method. SDS: Design Robinhood.
Do not let the candidate read this file. Give numbers and rules only when they ask.
Why this problem for this domain: it is two systems with opposite NFRs glued at one click. Quotes can be lossy and stale; the ledger cannot. That is “live progress dashboard” vs “official vote / settled position.” If they cache the quote path into the buy path, they fail a books-and-records interview too.
This is still an EM loop.
How to run it (45–60 min)
| Step | Time | Do not leave until |
|---|---|---|
| 1. Requirements | 8–10 min | Quote path ≠ order path; money exactly-once; exchange is external |
| 2. APIs | 3–5 min | Stream/read quotes; place order with idempotency; get positions |
| 3. High-level design | 12–15 min | Two subsystems drawn separately; they meet at place-order |
| 4. Deep dives | 12–15 min | Two of: retry buy, reserve/saga vs fill, stale quote at click, open spike |
| 5. EM close | 3–5 min | Who owns quotes vs ledger; KPI; this domain mapping |
If time dies, skip WebSocket fan-out math. Keep do not execute on a guessed fill and do not use the quote cache as the position.
Step 1 — Requirements
Open
“Users watch prices and tap buy. Are those the same system?”
If they draw one Kafka bus for both: “If a tick drops, is that as bad as a double fill?”
They should ask
- Real-time: prices, execution, or both?
- Do we match internally?
- Correctness on money/shares?
- Order types?
- Consistency of balances?
- Peak (market open)?
Answers when asked
| Question | Give them |
|---|---|
| Two paths | Both, built as separate paths. Prices stream continuously. Placement is a request. Execution is async at an external venue. |
| Matching | You do not run the matching engine. You own placement, routing, fills, balances. |
| Correctness | Never double-execute, never sell cash/shares the user does not hold, every fill reconcilable vs exchange reports. Dominating requirement. |
| Orders | Market and limit. Defer options/crypto/margin. |
| Balances | Strong. No eventual overdraft. |
| Quotes | Fast, best-effort. A dropped tick is replaced by the next. Stale is mostly harmless until they trade on it. |
| Peak | Market open is a synchronized spike. Quotes are a huge read fan-out; orders are smaller but spiky. Illustrative: quote messages ≫ order writes (thousands of orders/sec vs millions of quote msgs/sec). They should not scale both the same way. |
| Out of scope | Clearing T+1 internals, UI, vendor SDK trivia. |
Gate
They have said, in some words: lossy live quotes vs exactly-once ledger, glued at buy.
Weak: Redis pub/sub for prices and for balances. Strong: “I would rather the quote stutter than the ledger lie. At click we still place an order the exchange can reject.”
Step 2 — APIs
Want:
- Subscribe / get quotes (user’s symbols)
POST /orderswith idempotency key, side, type, symbol, qty/limitGET /orders/{id}— submitted / reserved / routed / filled / rejected / in_doubtGET /accounts/{id}/positionsand cash — from ledger, not from last quote
If they put last traded price on the order without a disclaimer: “The fill can be worse than what they saw. Who owns that conversation — product or a silent retry?”
Gate
Place-order is not the same resource as get-quote.
Step 3 — High-level design
Force two clusters of boxes.
Quote path (can be AP)
Feed → fan-out / sockets → clients. Loss, overwrite, last-tick cache OK.
Order path (CP)
API → idempotency → reserve cash/shares on a ledger → route to exchange → fills/webhooks → ledger entries → release leftover on partial/reject.
They meet: the button uses a displayed price; the authoritative fill comes back later.
Push
- “Where does the position number on the home screen come from after a fill?”
- “User’s quote is 2 seconds stale. They tap buy. What did we promise?”
- “Exchange ACK is lost. Client retries.”
Traps
| Trap | Follow-up |
|---|---|
| One topic for ticks and fills | “Consumer lag on quotes delays money?” |
| Execute locally off the last tick | “You said we don’t run the matching engine.” |
| Position = quote × qty in the client | “That’s a display, not a ledger.” |
| Timeout → mark filled because the UI already deducted | this domain analog: live grid that lies. |
Gate
Two paths. Ledger is not the quote cache.
Step 4 — Deep dives (pick 2–3)
A. Idempotent place
Same key → same order. Unique constraint. Retry returns current status.
B. Saga: reserve → route → fill
If exchange rejects, release the hold. If timeout, in-doubt until exchange report. Partial fills: ledger per fill, not one overwrite.
C. Stale quote at click
Want: order still goes as market/limit with their limit if any; UI can label “price may have moved”; do not invent a fill from the tick they saw.
D. Open spike
Quotes: drop ticks, coalesce, shed. Orders: do not shed silently; queue with backpressure, or degrade “new orders paused” rather than double-apply. Don’t onboard a new market-data feature the same morning as a known open.
E. this domain translation
Quotes ≈ labeled live vote progress / last-seen position. Ledger ≈ settled position / accepted vote. Agent/exchange ≈ books and records you do not control.
Step 5 — EM close
Listen for:
- v1: quotes streaming + market orders + ledger + in-doubt. Limit orders next. Not options.
- Topology: market-data squad vs trading/ledger squad vs venue adapter (dotted line, vendor). Conway: if one team owns the ticker and the ledger, the ticker wins and money loses.
- KPI: duplicate orders = 0; in-doubt age; position vs exchange breaks; quote delay SLO separate from order SLO
- Exec: “A jumpy ticker this week, or a ticker that can debit the wrong cash. We split the systems.”
- this domain: voting progress vs official tally; VRM two pictures; never vote on Redis.
Rubric
| Weak | Hireable | Strong | |
|---|---|---|---|
| Requirements | One real-time system | Names quotes vs orders | Ranks money above tick freshness |
| Design | Shared bus | Two paths | Reserve/saga + in-doubt vs exchange |
| EM | WebSocket deep dive | Mentions open spike | Separate teams/SLOs; this domain mapping |