Skip to main content

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)​

StepTimeDo not leave until
1. Requirements8–10 minQuote path ≠ order path; money exactly-once; exchange is external
2. APIs3–5 minStream/read quotes; place order with idempotency; get positions
3. High-level design12–15 minTwo subsystems drawn separately; they meet at place-order
4. Deep dives12–15 minTwo of: retry buy, reserve/saga vs fill, stale quote at click, open spike
5. EM close3–5 minWho 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​

QuestionGive them
Two pathsBoth, built as separate paths. Prices stream continuously. Placement is a request. Execution is async at an external venue.
MatchingYou do not run the matching engine. You own placement, routing, fills, balances.
CorrectnessNever double-execute, never sell cash/shares the user does not hold, every fill reconcilable vs exchange reports. Dominating requirement.
OrdersMarket and limit. Defer options/crypto/margin.
BalancesStrong. No eventual overdraft.
QuotesFast, best-effort. A dropped tick is replaced by the next. Stale is mostly harmless until they trade on it.
PeakMarket 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 scopeClearing 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 /orders with idempotency key, side, type, symbol, qty/limit
  • GET /orders/{id} — submitted / reserved / routed / filled / rejected / in_doubt
  • GET /accounts/{id}/positions and 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​

TrapFollow-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 deductedthis 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​

WeakHireableStrong
RequirementsOne real-time systemNames quotes vs ordersRanks money above tick freshness
DesignShared busTwo pathsReserve/saga + in-doubt vs exchange
EMWebSocket deep diveMentions open spikeSeparate teams/SLOs; this domain mapping