The interviewer just said…
“Design a live stock price feed for a watchlist page. Users see real-time prices for the symbols they follow. They can add stocks, remove stocks, and switch to a different page with a completely new set of symbols — all on the same connection.”
You have a whiteboard and roughly 45 minutes. This is a Low-Level Design (LLD) prompt — not a distributed streaming architecture on Kafka or Pulsar. The interviewer wants to see how you clarify scope, define APIs, pick the right in-memory structures, and explain fan-out and cleanup under concurrent access.
What you should do next
Pause. Do not open with “I’ll use Kafka with consumer groups” or “I’ll put Redis Pub/Sub in front of a WebSocket cluster.” That jumps past requirements and signals infrastructure before you have shown you understand the problem shape.
Your first move is conversational:
- Acknowledge the prompt.
- Ask clarifying questions (next chapter).
- State what you will deliver in the time box: REST APIs, one SSE connection per user, three in-memory maps, connect/disconnect lifecycle, symbol add/remove/replace, and a fan-out path for price ticks.
That sequence mirrors the classic LLD delivery framework: LLD delivery in a hurry.
The scenario in one paragraph
A user opens a stock watchlist page. The page establishes one persistent Server-Sent Events (SSE) connection and starts receiving live prices for roughly ten symbols. The user can:
- Add a stock — starts receiving prices for the new symbol on the same connection.
- Remove a stock — stops receiving prices for that symbol.
- Switch pages — the new page has a completely different set of symbols; old subscriptions are replaced entirely.
One SSE connection per user. All symbol updates flow through it. The SSE event’s name field (or payload field) carries the symbol so the client knows which price belongs to which stock.
What LLD wants from you here
This is a compact pub-sub problem that still hits real concurrency and lifecycle pain:
- Clarify — single JVM vs distributed, thread-safety, what happens on disconnect, whether missing one tick during a page switch is acceptable
- APIs — connect, watch (add/remove/replace)
- Data structures — three maps forming a bidirectional index between users and symbols
- Lifecycle — connect registers the emitter; disconnect cleans up every symbol registration
- Fan-out — given a price tick for
AAPL, find every user watchingAAPLand push to their emitter - Concurrency story —
ConcurrentHashMap, thread-safe sets, per-user lock for atomic replace
You are not building a multi-region market data platform. You are showing you can reason about registry design, bidirectional indexes, and cleanup on connection close on a problem small enough to implement in one service class.
What strong candidates deliver
When the interview is going well, you end up with something like this on the board:
APIs
| Endpoint | Description |
|---|---|
GET /connect?userId=U1 |
Opens the SSE connection for the user |
POST /watch?userId=U1&symbol=AAPL |
Add a symbol to the user’s watchlist |
DELETE /watch?userId=U1&symbol=AAPL |
Remove a symbol |
PUT /watch?userId=U1 body: ["MSFT","GOOGL","AMZN"] |
Replace entire watchlist (page switch) |
Three maps
userEmitters: Map<userId, SseEmitter> one SSE connection per user
userSymbols: Map<userId, Set<symbol>> which symbols a user is watching
symbolUsers: Map<symbol, Set<userId>> which users are watching a given symbol
Three traversal paths
- Fan-out:
symbol → symbolUsers[symbol] → userEmitters[userId] - Disconnect:
userId → userSymbols[userId] → symbolUsers[symbol].remove(userId) - Add/remove: update both
userSymbolsandsymbolUserstogether
Concurrency
ConcurrentHashMapfor all three mapsConcurrentHashMap.newKeySet()for the sets inside them- Per-user lock when replace must be atomic
That is the shape: one connection, bidirectional registry, fan-out on publish, then harden for concurrent access and page-switch races.
A 60-second preview of the client flow
1. GET /connect?userId=U1 → SSE connection open (no prices yet)
2. POST /watch?userId=U1&symbol=AAPL
3. POST /watch?userId=U1&symbol=TSLA
... (more symbols)
4. data: {"symbol":"AAPL","price":185.42} ← prices start flowing
The connection is open after step 1, but no prices arrive until the client registers symbols. Typically the frontend sends the initial watchlist immediately after establishing the SSE connection.
What not to lead with
| Tempting opener | Why it hurts |
|---|---|
| “I’ll use Kafka” | Wrong granularity — you have not defined the in-process registry yet |
| “I’ll use Redis Pub/Sub” | Same — skips the bidirectional index the interviewer wants to see |
| “I’ll open a WebSocket per symbol” | Violates one-connection-per-user constraint |
| “I’ll poll the database every second” | Not push; misses SSE lifecycle and fan-out design |
Name Kafka and Redis in the scale-out wrap-up after the single-JVM design is solid. The in-memory three-map design is the core of the interview.