HardProblem 23 of 36 in the system design series
Design Robinhood: A Stock Trading App with Live Prices and Safe Orders

A retail brokerage app lets ordinary people watch stock quotes tick in real time and place trades from their phone. The user sees a watchlist, a price chart and a big Buy button. What they do not see is that the app is a middleman: the actual buying and selling happens at an external exchange or market maker, and the app must relay orders there and reflect the outcome accurately.
This makes it a revealing interview question. Unlike most designs, the core action depends on a third party you do not control, and the stakes are money. A dropped order means a customer missed a trade; a duplicated order means they bought twice as many shares as intended. Both are serious, and both can happen through ordinary network retries if the system is careless.
At the same time, there is a very different scaling challenge on the read side: pushing live prices to millions of open screens, many times per second, without letting that traffic interfere with order handling. Good answers treat these as two separate systems with opposite priorities.
This is an interview practice design for a Robinhood-like service, built from first principles. It is not a description of how Robinhood is actually built.
Functional Requirements
Users need to see quotes update live for whatever stocks are visible, whether that is a watchlist or a single stock's detail page. They can place buy or sell orders as market orders, which execute at the best available price, or as limit orders, which only execute at a chosen price or better.
Users can also cancel an order that has not yet been filled, and track every order's status alongside the portfolio positions it affects. We explicitly do not build an order-matching engine; that is the exchange's job. Funding the account through deposits is also excluded. Drawing that line clearly tells the interviewer you understand where the brokerage's responsibility ends and the market's begins.
See live prices
Prices for the stocks on screen update in real time.
watchliststock page
Place an order
Buy or sell, as a market order or a limit order.
market · limit
Cancel an order
Cancel an open order that is not filled yet.
open orders
Track orders
See each order's status, and the portfolio it changes.
status · positions
- Out of scope: the matching engine itself (the exchange runs it), deposits
Non-Functional Requirements
Price updates should reach the screen within about 200 ms of the change, which is fast enough to feel live without demanding the latency of a professional trading terminal. Orders demand strong consistency: an order accepted by the app must never vanish and must never be executed twice, even when clients retry or servers crash mid-request.
Availability during market hours is critical, targeting 99.99%. An outage during a market sell-off is not just inconvenient; customers who cannot exit positions can lose real money. Finally, traffic is spiky. The opening bell at 9:30 AM and major news events produce sharp bursts in both orders and price viewers, so capacity must be planned for the peak, not the average.
Fresh prices
Price changes reach the screen quickly.
< 200 ms
Correct orders
Never lose an order, never place it twice.
strong consistency
High availability
Must stay up during market hours, at any cost.
99.99% in hours
Spiky scale
Huge bursts at market open and on big news days.
9:30 AM spike
Scale Estimation
Assume 20 million daily active users placing around 5 million orders a day. Because trading is concentrated into a 6.5-hour session, that averages roughly 200 orders per second, with bursts into the low thousands right at the open. That order rate is modest; a well-tuned relational database handles it easily.
The price side is where the numbers explode. Assume 2 million users have a price screen open at peak, each holding a live connection and viewing about 5 symbols, with one update per symbol per second. That is around 10 million outbound price messages every second. The lesson is clear: orders need correctness more than throughput, while price delivery is a massive fan-out problem that needs its own infrastructure.
20million
Daily active users
5million
Orders per day
≈ 200 / s over 6.5 h
2million
Open connections
at peak, live price screens
10million
Price pushes per sec
2M users × 5 symbols × 1/s
Orders are small. Price fan-out is the real scale problem.
Core Entities & API
The model has four entities: User, Symbol with its latest Quote, Order, and Position (the shares a user currently owns). Keeping Position separate from Order lets the portfolio be updated only when fills actually arrive.
Prices flow over a WebSocket at /prices; the client sends the list of symbols it wants, such as AAPL and TSLA, and receives a stream of symbol, price and timestamp messages. POST /orders takes symbol, side, quantity, type, an optional limit price and a client-generated idempotency key, returning an order ID in PENDING. DELETE /orders/{order_id} returns CANCEL_REQUESTED rather than cancelled, because only the exchange can confirm a cancel actually happened before a fill. GET /orders?status=open lists open orders with their filled quantity.
Core entities
UserSymbol / QuoteOrderPosition
API
- WS /prices subscribe { symbols: [AAPL, TSLA] }→ stream { symbol, price, ts }
- POST /orders { symbol, side, qty, type, limit?, key }→ { order_id, status: PENDING }
- DELETE /orders/{order_id}→ { status: CANCEL_REQUESTED }
- GET /orders?status=open→ [ { order_id, status, filled_qty } ]
High-Level Design
The architecture has two independent lanes. On the order lane, clients go through an API gateway for authentication and rate limiting to an order service. That service checks buying power or share holdings, writes the order to Postgres as PENDING, and passes it to an exchange gateway. The gateway is the single component that holds a FIX session with the exchange, sends orders, and receives execution reports that it writes back as status changes and fills.
On the price lane, a market data feed from the exchange or a vendor streams ticks into a price service, which publishes each one to a Redis pub/sub channel per symbol. A fleet of push servers listens to the channels their connected users care about and forwards updates over WebSockets. Because the lanes share nothing on the hot path, a flood of price ticks during a volatile session cannot delay someone trying to sell.
Two separate paths → a price storm never slows down orders
Deep Dive 1 — Streaming Live Prices
Polling every second is the simplest option, but quotes go stale between requests and millions of clients generate an enormous volume of mostly redundant calls. Server-Sent Events push data over plain HTTP and suit one-way streams, yet changing the set of watched symbols means a new request each time a user scrolls to a different stock.
WebSockets win because a single long-lived connection can carry both directions: the client adjusts its symbol list on the fly and the server streams updates back. Fan-out is efficient because each push server joins a symbol's Redis channel once, regardless of how many local users watch it, then copies each tick to those connections in memory.
Conflation keeps the load sane. If a symbol changes fifty times in one second, only the latest value is sent, at most once per second, which saves bandwidth and battery without hurting a retail user. When a push server fails, its clients reconnect elsewhere, restate their symbol list and receive a fresh snapshot, so nothing needs to be persisted per connection.
Client polling
- + Very simple
- − Stale between polls
- − Millions of wasted calls
Server-Sent Events
- + Simple one-way push
- + Plain HTTP
- − New symbols = new request
WebSocketChosen
- + Change symbols any time
- + Tiny per-message cost
- − Stateful connections
- Each push server subscribes once per symbol, then fans out locally
- Conflation: only the latest price per symbol, at most once a second
- Server dies → client reconnects, re-subscribes, gets a fresh snapshot
Deep Dive 2 — Order Flow
When a user taps Buy, the order service first verifies buying power and places a hold on the funds (or, for a sell, on the shares). Without that hold, a user with $1,000 could fire off two $1,000 orders in quick succession and overspend. The order is then written as PENDING before anything leaves our system, so even if a server dies a moment later there is a durable record to recover from.
The exchange gateway transmits it as a FIX message carrying a client order ID that we generate. From there, the order behaves as a state machine: PENDING to SUBMITTED, then PARTIAL and FILLED, or REJECTED or CANCELED. Execution reports only ever advance it. On each fill, the user's position is increased and any unused portion of the hold is released, for example when a limit buy executes below its limit. Finally, a push notification goes out and the app refreshes the portfolio.
- 1Validate & reserveCheck buying power; hold the cash or shares
- 2Save as PENDINGWrite the order before anything leaves our system
- 3Send to exchangeGateway sends FIX order with our client order ID
- 4Track the stateExecution reports move the order forward
- 5Update positionsOn fill: add shares, release the unused hold
- 6Notify the userPush notification and refresh the app
- PENDING → SUBMITTED → PARTIAL → FILLED (or REJECTED / CANCELED)
Deep Dive 3 — Consistency & Idempotency
Duplicate protection starts on the device. The app generates an idempotency key per order and reuses it on any retry, so a request repeated after a timeout returns the original order instead of creating another. The gateway applies the same idea outward: a resend always carries the same client order ID, and the exchange rejects duplicate IDs, so one order can never fill twice.
Crash recovery is cautious by design. A restarted gateway first asks the exchange where each outstanding order stands, then decides what, if anything, needs resending. Execution reports land in Kafka, a durable log, and consumers record each execution ID they have applied, so replaying the log after a failure does not add shares twice. As a final backstop, a nightly job compares our orders and positions to the official broker reports and corrects any discrepancy.
Idempotency key from the app
A retried request with the same key returns the same order, not a new one.
Same client order ID on every resend
The exchange rejects a duplicate ID, so an order can never fill twice.
Gateway recovery after a crash
Ask the exchange for the status of open orders before resending anything.
Execution reports through Kafka
Durable log; consumers apply each execution ID only once.
End-of-day reconciliation
Match our orders and positions with the broker's reports; fix any gap.
Trade-offs
Choosing WebSockets over SSE buys flexible two-way symbol management but brings stateful servers that need connection draining and reconnect handling. Conflation dramatically cuts traffic, at the cost of users not seeing every single tick, which is acceptable for a consumer app.
The design deliberately mixes consistency models: strict for orders and positions, relaxed for prices. Paying for strong guarantees only where money moves keeps the price lane cheap and fast. Reserving funds up front prevents overspending, though the held cash is unavailable until the order fills or is cancelled. The short version: prices can be quick and slightly lossy, but every order must be exact.
WebSocket vs SSE
Flexible two-way subscriptions — but stateful servers to manage.
Conflating prices
Far less traffic and battery — but users don't see every single tick.
Strong for orders, eventual for prices
Pay the cost of consistency only where money moves.
Reserve funds up front
No overspending — but cash stays locked until the order fills or is canceled.
Prices: fast and lossy is fine. Orders: exact, every single time.
Key takeaways
- Order volume is small; the real scale challenge is fanning out about 10 million price messages per second.
- Keep the price lane and order lane separate so a tick storm can never slow down trading.
- WebSockets plus per-symbol pub/sub and conflation deliver live quotes efficiently.
- Reserve funds, persist as PENDING first, and drive orders through a forward-only state machine.
- Idempotency keys, stable client order IDs, deduped execution reports and nightly reconciliation stop double fills.
Interview questions
How do trading apps stream live stock prices to millions of users?
A price service ingests market data ticks and publishes them to a pub/sub channel per symbol, for example in Redis. Push servers hold WebSocket connections with clients, join each symbol's channel once, and fan updates out locally. Conflation sends only the latest price per symbol about once a second, which cuts traffic and battery use while still feeling live.
How do you prevent duplicate stock orders when a user taps Buy twice?
The app attaches an idempotency key to each order and reuses it on retries, so the server returns the existing order instead of creating a new one. Downstream, the exchange gateway always resends with the same client order ID, which the exchange rejects as a duplicate. Execution reports are applied once per execution ID, so even replays cannot double a position.
What is an exchange gateway in a brokerage system design?
It is the single service that talks to the external exchange, usually over the FIX protocol. It owns the session, sequence numbers and throughput limits, sends orders tagged with client order IDs, and turns execution reports into status updates and fills. Centralising this keeps other services simple and gives one place to handle recovery after a crash.
Why use WebSockets instead of polling for live stock prices?
Polling leaves prices stale between requests and wastes millions of calls when nothing changed. Server-Sent Events push efficiently but need a separate request whenever the watched symbol list changes. A WebSocket keeps one connection open, lets the client change symbols at any time, and carries each update with very little overhead, which suits a scrolling watchlist.
How does a trading app handle the market-open traffic spike?
Scale push servers ahead of 9:30 AM rather than reacting after load arrives, conflate price updates so bursts of ticks do not multiply outbound messages, and queue orders in front of the exchange gateway so it can submit them at a steady, permitted rate. Keeping the order path separate from the price path means a surge in viewers never blocks trades.
Keep practising
- Learn the building blocks in the free system design course.
- Rehearse answers with system design interview questions.
- See which open jobs fit your skills: upload your resume.