Design a service that shows the latest stock prices worldwide
Ingest ticks from every exchange and show every user the latest price within a second.
- Stream processing — Windows, watermarks and late events: computing over data as it arrives.
- WebSockets, long polling and SSE — Keeping a connection open so the server can speak first.
- Caching strategies — Read-through, write-through, write-back, TTLs and invalidation.
- Time-series storage — Append-only metrics with compression, downsampling and retention.
- Fan-out on write versus fan-out on read — Pushing a post to every follower's inbox, or assembling the feed when asked.
01The design, in the six-step framework
1. Requirements
Functional
- Ingest price ticks for every listed symbol from every exchange worldwide.
- Show any user the latest price for symbols they watch, updating live while the page is open.
- Serve intraday charts (one-minute bars) and daily history.
- Alerts when a price crosses a user's threshold.
- Out of scope: order entry, level-two order books, paid real-time entitlements.
Non-functional
- 100,000 symbols; 1 million ticks a second at a global peak (US open overlapping Europe).
- Latest price visible to a user within 1 s of the exchange tick; charts within 5 s.
- 50 million users; 5 million concurrently watching; a watchlist holds 20 symbols.
- Never show a stale price as live: a symbol whose feed stopped must be marked.
- Availability 99.99 percent for reads; ingestion is redundant per exchange.
Ticks: 1 M/s × 50 B = 50 MB/s raw, 4 TB a day; one-minute bars compress this to 100,000 symbols × 390 bars a day × 40 B ≈ 1.5 GB a day.
Fan-out: 5 M users × 20 symbols = 100 M subscriptions; a hot symbol ticking 100 times a second with 1 M watchers would be 108 pushes a second if every tick were sent, so throttle to at most 2 updates per second per symbol per user: at most 2 × 100 M = 200 M pushes a second worst case, in practice far less since most symbols tick slowly.
The latest-price table is 100,000 rows × 100 B = 10 MB; it fits in every edge server's memory.
History: 10 years of daily bars for 100,000 symbols is 250 M rows, a few tens of gigabytes.
2. Core entities
- Symbol: symbol_id, exchange, currency, trading hours, state.
- Tick: symbol_id, price, size, exchange timestamp, received_at, sequence.
- Latest quote: symbol_id, price, change, as_of, feed_health.
- Bar: symbol_id, interval, open, high, low, close, volume, start.
- Watchlist and alert: user_id, symbols; alert rules with thresholds and state.
3. API
GET /quotes?symbols=…: latest prices with as_of and health. Called on page load.WS /streamwith a subscribe message listing symbols; the server pushes throttled quote updates. Called by open pages.GET /bars/{symbol}?interval=1m&from=…&to=…: chart data.PUT /users/{id}/watchlist;POST /users/{id}/alertswith symbol and threshold.
4. High-level design
One tick to one screen:
- Two feed handlers per exchange consume the exchange's feed; each normalises the message and the pair dedupes by exchange sequence number so a handler failure loses nothing.
- The tick is published to the tick stream partitioned by symbol.
- A consumer updates the latest-quote table, which is replicated into the memory of every push server; another builds one-minute bars into the time-series store; a third evaluates alert rules keyed by symbol.
- A push server holding a user's WebSocket sees the change for a subscribed symbol and, at most twice a second per symbol, sends the latest quote down the socket; a page load first fetches quotes over HTTP, then subscribes.
- If a symbol's feed goes quiet during trading hours beyond a threshold, the handler marks it stale and the UI shows the last price greyed with its time.
5. Deep dives
How does a hot symbol with a million watchers not produce a hundred million pushes a second?
| Option | Behaviour | Trade-off |
|---|---|---|
| Push every tick to every subscriber | 108 messages a second for one symbol | Impossible |
| Subscriber-side polling | Clients poll every second | 5 M × 20 polls a second; mostly unchanged data; still heavy |
| Server-side coalescing per symbol per connection | Each push server keeps the latest value and sends at most N updates a second per symbol, only when changed | A viewer sees 2 updates a second instead of 100; nobody can read more anyway |
Coalescing on the push server is the choice; it is fan-out on read with a rate cap, and because the latest table is local to every push server, the fan-out is a memory read and a socket write, with no per-user state anywhere else.
Why replicate the latest table everywhere instead of a central cache?
A central cache would take 100 M subscription lookups a second at peak and add a network hop to every push. The table is 10 MB, changes at up to 1 M writes a second but each write is tiny, and the stream already delivers every change to any consumer; so each push server consumes the stream and keeps its own copy, falling at most milliseconds behind. Adding push capacity is adding stateless servers that subscribe to the stream, which is the simplest scaling story available. A central cache remains for the HTTP quotes endpoint and for cold starts of push servers.
How is "latest" kept correct when ticks arrive out of order or twice?
Exchange feeds carry sequence numbers per symbol; the handler keeps the last sequence per symbol and drops duplicates and anything older than what it has already published. Two handlers racing on the same exchange are resolved the same way downstream: the latest-table update is conditional on the sequence being newer. Timestamps from the exchange are kept for display; ordering uses sequences, never clocks.
How are charts built and served without touching the raw ticks?
The bar aggregator keeps an open one-minute bar per symbol in memory, updating open, high, low, close and volume per tick, and writes the bar when the minute closes (with a short grace for late ticks). Charts read bars from the time-series store, which stores them by symbol and time so a day of one-minute bars is one contiguous read; longer intervals are rolled up from the minute bars nightly. Raw ticks are archived to cold storage for a day and then kept only in sampled form, since bars answer every product question.
How do alerts fire within a second without scanning every rule on every tick?
Rules are indexed by symbol in memory, as two sorted lists per symbol: thresholds above and below the current price. A tick that moves from 99.5 to 100.2 fires exactly the rules with thresholds in that range, found by two binary searches, so a symbol with a million rules costs microseconds per tick. Fired rules are marked and handed to the notification system; a rule re-arms only when the user says so, which prevents a price oscillating around 100 from sending twenty alerts.
6. What a strong answer sounds like
- Point out that the data is small (10 MB of latest prices) and the fan-out is huge, so the design replicates the data to the edge and coalesces the pushes.
- Quote 1 M ticks a second in, at most 2 updates a second out per symbol per user, and the 100 M subscription count.
- Insist on sequence numbers over timestamps for ordering and dedup, and on marking stale feeds rather than showing old prices as live.
- Expect follow-ups on entitlements (delayed versus real-time), on market open bursts, and on how the graph for "latest prices" in the reported phone screen maps to bars.
- A common mistake is to store every tick in a database and query the max timestamp per symbol; the latest price is a stream state, not a query result.
02Where this question was reported
- IGotAnOffer · EM list
- IGotAnOffer · EM high-level list
- Blind · Stock trading system design
- Taro · Phone screen: design a graph system that gets the latest stock prices