A stock trading app has one job that makes it different from most apps: it moves real money based on prices that change several times a second. A slow feed is annoying in a social app. In a trading app, a price that's two seconds old can cost someone money.
That raises the bar for the mobile client. Prices must arrive fast without draining the battery. Charts must stay smooth while data streams in. And the app must never show a wrong or stale number as if it were live.
This walkthrough answers "Design a stock trading app" the way you would in a 45-minute interview, in five steps:
- Clarify the requirements
- Design the API and data model
- Sketch the client architecture
- Deep dive into the hard problems
- Wrap up with trade-offs
Step 1: Clarify the Requirements
Start by asking what can be traded. Stocks only, or options, futures, and crypto too? Each one changes the order types, the risk checks, and the screens. For this session, assume stocks only, on a few international markets.
Functional requirements
- See a portfolio with live prices and total value
- View a stock's details: live price, day high and low, and a price history chart
- Place buy and sell orders, and cancel open orders
- See order status change in real time (open, filled, cancelled)
- Set price alerts and get a push notification when a stock reaches the target
Non-functional requirements
| Requirement | Target | What it means for the client |
|---|---|---|
| Latency | Live prices on screen within about a second | A persistent connection for market data |
| Correctness | Money is never rounded wrongly | Exact decimal types, never floating point |
| Smoothness | No dropped frames while prices stream in | Buffer updates instead of redrawing on every tick |
| Security | Financial data protected in transit and on disk | TLS everywhere, nothing sensitive stored in plain text |
| Scale | About 500,000 daily users, thousands of symbols | Subscribe only to the symbols on screen |
Out of scope
Set aside sign-up and identity verification, deposits and withdrawals, options and futures, and the exchange's matching engine. Mention them as extensions.
The screens
Three screens cover the scope:
- Home: account balance, a portfolio value chart, holdings, and recent orders
- Stock details: live price, key stats, a history chart, and Buy and Sell buttons
- Place order: a bottom sheet to choose quantity, review the estimated cost, and confirm
Step 2: Design the API and Data Model
A trading app has two kinds of traffic. The user starts some of it: placing an order, loading the portfolio, fetching price history. The market starts the rest: prices change, orders fill. The design uses REST for the first kind and a WebSocket for the second.
REST endpoints
GET /v1/portfolio
GET /v1/stocks/{symbol}
GET /v1/stocks/{symbol}/history?interval=1m&period=1d
GET /v1/orders?status=open&page=1&limit=20
POST /v1/orders
Idempotency-Key: 9b2e...41
{ "symbol": "AAPL", "side": "BUY", "type": "LIMIT", "quantity": "10", "limitPrice": "189.40" }
→ 201 Created { "orderId": "ord_7731", "status": "OPEN" }
DELETE /v1/orders/{orderId}
POST /v1/alerts
{ "symbol": "AAPL", "targetPrice": "195.00", "direction": "ABOVE" }
Notice that quantity and limitPrice are strings, not numbers. JSON numbers are usually parsed as floating point, and that loses precision. Sending money as a string and parsing it into a decimal type keeps every digit exact. More on that in the deep dives.
Orders use page-based pagination, not a cursor. Orders rarely change after they're created, and people mostly look at the latest few, so the shifting-page problem that feeds have doesn't really apply here.
WebSocket messages
The app opens one secure socket and subscribes to the symbols it needs:
{ "action": "subscribe", "channel": "prices", "symbols": ["AAPL", "MSFT", "GOOG"] }
The server then streams updates:
{ "type": "price", "symbol": "AAPL", "price": "189.42", "ts": 1761980400123 }
{ "type": "order_update", "orderId": "ord_7731", "status": "FILLED", "filledPrice": "189.38" }
The ts field comes from the server. The app uses it to show "last updated" times and to detect stale data, never the phone's own clock.
Step 3: Sketch the Client Architecture
UI LAYER
HomeScreen → HomeViewModel
StockDetailScreen → StockDetailViewModel
PlaceOrderSheet → PlaceOrderViewModel
│ actions ↓ ↑ state
DATA LAYER
PortfolioRepository (holdings, balance)
StocksRepository (live prices, history)
OrdersRepository (place, cancel, track)
│
├─ HttpClient REST calls
├─ MarketSocket WebSocket: prices, order updates
├─ HistoryCache disk cache with expiry
└─ PushClient FCM / APNs for alerts
Each screen has a ViewModel that exposes one state object. Each kind of data has one repository, and the screens only talk to repositories.
Different data, different caching
This is where a trading app differs from a feed or a chat app, so explain it clearly:
| Data | Where it's kept | Why |
|---|---|---|
| Live prices | Memory only | Writing them to disk wastes battery and could show an old price as current after a restart |
| Price history | Disk cache with an expiry | It doesn't change for past periods, and it saves data on every open |
| Portfolio and orders | Fetched on open, then updated by socket events | The server's record is the truth, because real money is involved |
| Display preferences | Key-value storage | Small, flat settings like the chart's default range |
The key rule: a live price on screen must always come from the current session. If the app can't confirm a price is fresh, it says so.
Step 4: Deep Dive into the Hard Problems
1. Money must never be a Double
This is the first thing I'd raise in any trading interview. Binary floating point can't represent most decimal fractions exactly:
println(0.1 + 0.2) // 0.30000000000000004
For a price, that error is tiny. Multiply it across quantities, fees, and thousands of orders, and the totals drift. Use an exact decimal type and set the rounding rule explicitly:
val price = BigDecimal("189.42")
val quantity = BigDecimal("10")
val fee = BigDecimal("0.15")
val total = price.multiply(quantity)
.add(fee)
.setScale(2, RoundingMode.HALF_EVEN) // 1894.35
On iOS, use Decimal. Two more rules go with this. Parse money from strings, never from JSON numbers. And let the server do the final calculation: the app shows an estimated cost, and the confirmed amount comes back with the order.
2. Streaming prices without dropping frames
During market hours a busy symbol can tick several times a second. If every tick goes straight to the UI, the main thread redraws constantly, the chart stutters, and the battery drains.
The fix is to buffer and coalesce. Ticks arrive in the repository, which keeps only the latest price per symbol. The UI reads a snapshot at a fixed rate:
class StocksRepository(private val socket: MarketSocket) {
private val latest = ConcurrentHashMap<String, Tick>()
init {
socket.ticks
.onEach { tick -> latest[tick.symbol] = tick } // keep only the newest
.launchIn(ioScope)
}
// The UI collects this: at most 4 updates a second, whatever the tick rate
fun prices(symbols: Set<String>): Flow<Map<String, Tick>> =
ticker(periodMs = 250)
.map { latest.filterKeys { it in symbols } }
.distinctUntilChanged()
.flowOn(Dispatchers.Default)
}
Four updates a second looks live to a person, while the app does a fraction of the work. A few more rules complete the picture:
- Subscribe only to what's on screen. When the user leaves a stock's page, unsubscribe from it. Thousands of symbols exist, but a screen shows a handful.
- Close the socket in the background. Price alerts work through push notifications, so there's no reason to stream prices nobody can see.
- Never validate an order against the throttled UI price. The display rate is for people. The server checks the real price when it executes the order.
3. Rendering charts
One trading day at one-minute resolution is 391 points, and five years of daily prices is over 1,250. There are four realistic ways to draw them:
| Approach | Strength | Weakness |
|---|---|---|
| Draw it yourself (Canvas, Core Graphics) | Fastest, full control | Built twice, once per platform |
| Native chart library | Quick to adopt, gestures included | You depend on someone else's maintenance |
| Server-rendered images | Almost no work on the phone | No interactivity, and every zoom needs the network |
| WebView with a web chart library | One chart for web, Android, and iOS | Heavier, and it needs a JavaScript bridge |
If the company also has a web app, a WebView chart keeps all three platforms identical. It needs care to stay fast:
- Prepare the data on a background thread before passing it to the chart, so JavaScript does no heavy array work.
- Load a coarse version first, and add detail as the user zooms in.
- Send new prices in batches through the bridge, at the same 250 ms rate as above, and append to the chart instead of redrawing it.
- Keep the order flow out of the WebView. Placing an order is a native screen, so it stays fast even if the chart is busy.
4. Placing an order safely
An order is the one action where a duplicate is truly expensive. Imagine the user taps Buy, the request times out, and the app retries. If the first request actually reached the server, the user now owns twice the shares they wanted.
The Idempotency-Key header prevents this:
- When the order sheet opens, the app generates a UUID for this order.
- Every attempt to submit the order sends the same key.
- If the server has already seen that key, it returns the original order instead of creating a new one.
The UI is honest about state, too. Unlike a Like button, an order is never shown as done optimistically. After the user confirms, the sheet shows "Submitting...", then the real status from the server. A filled order shows the server's filledPrice, which can differ from the estimate.
5. Showing stale prices honestly
The worst bug in a trading app is a frozen price that looks live. Every price on screen carries the server timestamp of its last update. If the socket disconnects, or no update has arrived for a few seconds during market hours, the UI changes:
- The price turns grey with a "Last updated 10:42:13" label.
- The Buy and Sell buttons warn that prices may be delayed.
- The app reconnects with backoff, then refetches current prices over REST before showing anything as live again.
Outside market hours, the app shows the closing price with a "Market closed" label instead.
Step 5: Wrap Up
| Decision | Why | Trade-off |
|---|---|---|
| REST for orders, WebSocket for prices | Clear results for orders; a fast stream for market data | Two channels to maintain |
| Decimal types and string-encoded money | Exact amounts, no drift | Slightly slower math and more parsing code |
| Coalesce ticks to 4 updates a second | Smooth UI and lower battery use | The screen can be up to 250 ms behind the latest tick |
| Live prices in memory only | No stale price shown after a restart | Prices must be refetched on every launch |
| WebView charts | One chart across web and mobile | Extra memory and a JavaScript bridge to manage |
| No optimistic UI for orders | The user always sees the real order state | Placing an order feels slightly slower |
With more time, natural extensions are:
- Biometric confirmation before large orders
- Regional compliance checks, such as pattern day trading rules
- Watchlists with their own price subscriptions
- Offline support for history, so charts for past periods open without a connection
- Accessibility, such as announcing price changes to screen readers without flooding them
Don't call the design perfect. Each choice above has a cost, and naming it shows you understand the design.
Top comments (0)