Course Content
Mobile System Design Interview
11 sections · 23 lessons
Stock Trading: charts, safe order placement and follow-ups
The previous lesson got prices from the stream to the watchlist at a readable, bounded rate. This lesson takes the two deep dives that remain — the chart on the symbol screen and the order button beneath it — and then the follow-ups that interviewers use to separate people who have built a trading surface from people who have not.
A price chart is a lot of points and very few pixels. Almost every chart performance problem comes from forgetting the second half of that sentence.
Rendering charts: the arithmetic
A five-year daily chart is roughly 1,250 points. A one-day intraday chart at one-second resolution is about 23,400. A full tick series for an active symbol can be hundreds of thousands.
The chart is about 350 points wide on a phone. At a 3× screen scale that is roughly 1,050 pixels. Drawing 23,400 points into 1,050 pixel columns means more than twenty points land in every column, and twenty-two of every twenty-three are drawn on top of each other and never seen.
Downsample to the pixels available
Reduce the series to about one to two points per pixel column before drawing. Two approaches:
Bucket by column. For each pixel column, keep the open, high, low, and close of the points falling in it. Four points per column preserves the visual envelope — the spikes stay — and is exactly what a candlestick needs anyway.
Largest-triangle-three-buckets. A downsampling algorithm designed for visual fidelity in line charts; it picks the point in each bucket that best preserves the shape of the line. Worth naming if you know it, and worth not bluffing if you do not.
Either way, downsampling happens once, off the main thread, cached against the series version and the target width. It is redone when the range changes or the view is resized, not on every frame.
Incremental updates instead of full redraws
While the market is open, one new point arrives per tick. Redrawing 1,050 columns for one new point is wasteful.
Split the chart into two layers: a static layer holding the historical series, redrawn only when the range or data set changes, and a live layer holding the last few points, the current price line, and the crosshair, redrawn at the coalesced 10 Hz rate from Throttling and coalescing updates. The expensive layer redraws rarely; the cheap layer redraws often.
Panning and zooming needs the same discipline: transform the already-rasterised layer during the gesture and re-downsample once when the gesture ends. Re-downsampling on every frame of a pinch is the classic cause of a chart that stutters under the finger.
Platform charting or custom drawing
| Platform or library charting | Custom drawing | |
|---|---|---|
| Build cost | Low | High |
| Control over the render loop | Limited | Complete |
| Handles 20k+ points | Often poorly | Yes, if you downsample |
| Custom interactions | Constrained | Anything |
The honest recommendation: start with a library, measure with a realistic worst-case series on a low-end device, and move to custom drawing only for the chart that actually needs it — usually the intraday one. Saying "I'd measure first and only hand-roll the one chart that fails" is a stronger answer than committing to either option on principle.
Offline: the last fetched series renders from the local database with its capture time shown. The live layer is empty and the chart is visibly truncated at the last known point rather than drawing a flat line to the present, which would be a fabricated price.
Order placement safety
Everything else in this case study is about display. This is about a write that moves the user's money across a network that drops requests, and it is where "we retry on failure" becomes the wrong answer.
The ambiguous failure
The user taps Buy. The request goes out. No response arrives.
There are three possible truths, and the client cannot distinguish them:
- The request never reached the server. No order exists.
- The server received it, placed the order, and the response was lost. An order exists.
- The server received it and rejected it. No order exists.
A blind retry is correct in cases 1 and 3 and places a second order in case 2. The user who wanted 10 shares now owns 20, at a price they did not agree to, and it is genuinely their money.
The three-part fix
1. A client-generated idempotency key. The client generates a unique key when the user confirms the order and sends it with the request. The server stores it against the resulting order. A second request carrying the same key returns the original order rather than creating a new one. This turns an unsafe retry into a safe one — but only for the same attempt, so the key is generated once at confirmation and reused across every retry of that attempt, and never reused for a new order.
2. Never auto-retry silently. Even with idempotency keys, an order is not something to retry in the background without the user knowing. Retry once automatically at most, then move the order into an explicit unresolved state and tell the user.
3. Reconcile rather than resend. The safest response to an ambiguous outcome is not to send the order again — it is to ask:
GET /v1/orders?client_order_id=<the key we generated>→ 200 { order_id, status: "filled" | "working" | "rejected" }→ 404 { } // the server has never seen it — safe to submitThat single endpoint resolves all three truths, and asking for it is the strongest thing you can say in this part of the interview.
The confirmation state machine
DRAFT → CONFIRMED (user tapped, key generated, row persisted) → SUBMITTING → ACCEPTED → WORKING → FILLED / PARTIALLY_FILLED / CANCELLED → REJECTED (server said no — terminal, show the reason) → UNKNOWN (no response — NOT terminal; reconcile on next connection)UNKNOWN is the state candidates omit and interviewers ask about. It must be persisted, must be visible to the user as "submitted — confirming status", and must trigger a reconciliation call on every app launch and every reconnect until it resolves.
Offline: order placement is disabled, not queued. This is the sharpest contrast with the chat app's outbox. A chat message sent an hour late is fine; a market order executed an hour late at an unknown price is not. Grey the button, say why, and let the user set a limit order instead if the product supports one — an instruction that is safe to submit later because it carries its own price condition.
Follow-ups
Stale-data indication
The most important follow-up in this case study, and the one with a rule worth stating flatly: showing a wrong price is worse than showing no price.
Each symbol carries a last-updated timestamp. A watchdog marks it stale after a few seconds of silence while the market is open. Stale rendering is desaturated, timestamped, and has no flash animation — the green and red flash means "this changed just now", so animating a stale value actively lies. A connection banner sits above the list, and trading actions are disabled while the whole feed is stale.
Market closed and after-hours
The client needs the market's session state, from the server, because it depends on exchange calendars and holidays that the client should not encode. Three states with different behaviour: open (live, watchdog active), closed (show the closing price with its date, no staleness warning — the data is correct, the market is shut), and extended hours (live but lower volume, clearly labelled, often with different trading permissions).
The bug this prevents is a watchdog screaming "stale data" all weekend because no ticks have arrived for 60 hours.
Price alerts via push
The alert condition is evaluated on the server, not the client. A client cannot watch a price while suspended, and a design that says "we poll every minute to check the alert" is both against the platform rules and a serious battery cost.
The client registers the alert, the server evaluates it against the feed it already has, and delivery is a push notification. The push carries the symbol, the condition, and the price at trigger time, so the notification is meaningful even if the app cannot open. This is a push where the payload is the message, unlike in the chat app — the alert is a fact about a moment, and fetching a fresher price when the user taps would show a different number than the one that triggered it.
Regulatory disclosure on the client
Financial apps carry client-side compliance requirements, and an interviewer at a fintech will notice if you have never thought about them.
- Delayed data must be labelled as delayed, visibly, wherever it appears.
- Order confirmations typically must be presented and retrievable, which makes the local order record a compliance artefact rather than a cache — so it is not evictable.
- Risk disclosures may need to be acknowledged before certain order types.