DEV Community

Shakibaenur
Shakibaenur

Posted on

Design a Stock Trading App: A Mobile System Design Walkthrough

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:

  1. Clarify the requirements
  2. Design the API and data model
  3. Sketch the client architecture
  4. Deep dive into the hard problems
  5. 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" }
Enter fullscreen mode Exit fullscreen mode

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"] }
Enter fullscreen mode Exit fullscreen mode

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" }
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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:

  1. When the order sheet opens, the app generates a UUID for this order.
  2. Every attempt to submit the order sends the same key.
  3. 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)