CASE STUDY

Stock Trading Platform (Zerodha / Robinhood)

6 min read·1,044 words·Advanced

How to use this case study

SDE-2 / Mid

Explain order types, the order lifecycle (states), how funds are checked before placing an order, and how status updates come back from the exchange.

SDE-3 / Senior

Go deeper on the ledger with holds, reconciling with exchange reports, idempotent order placement, market-open spikes and streaming quotes to millions of users.

Staff / Principal

Discuss risk checks, exchange connectivity and failover, regulatory audit trails, multi-exchange routing and the design of an in-memory order manager (LLD).


0) Problem Restatement

Design a brokerage app where users see live stock prices, place buy and sell orders, and track their orders, holdings and balance. The broker does not match trades itself. It sends orders to a stock exchange (like NSE or NASDAQ), which matches buyers and sellers and reports back fills (executions).

Key challenges:

  • Never let a user spend money they don't have.
  • Keep every order's status correct, even when messages from the exchange are delayed or repeated.
  • Survive the huge spike when the market opens.

Asked at: Bloomberg, Databricks, Goldman Sachs, Uber — 5 candidate reports between Oct 2025 and Jun 2026.

1) Requirements

1.1 Functional

  • Live quotes for stocks.
  • Place orders: market (buy now at the best price), limit (buy only at ₹100 or lower), stop-loss. Modify and cancel orders.
  • Show order status: open, partially filled, filled, cancelled, rejected.
  • Show holdings, P&L and available funds.

1.2 Non-Functional

  • Correctness of money and positions above everything.
  • Low latency from order click to exchange (tens of milliseconds inside our system).
  • High availability during market hours.
  • Full audit trail for regulators.

1.3 Scale Estimates

  • 10M active users. At market open, 50K orders/sec for the first minutes.
  • Quotes: 5,000 stocks with many ticks per second each, fanned out to about 2M connected users.
  • 20M orders/day. Each order has ~5 events, so about 100M order events/day.

1.4 API Design

  • POST /v1/orders with header Idempotency-Key and body { symbol, side: buy, type: limit, qty: 10, price: 2450.50 }{ order_id, status: "open" }
  • DELETE /v1/orders/{id} (cancel), PATCH /v1/orders/{id} (modify)
  • GET /v1/orders?status=open, GET /v1/portfolio
  • WebSocket /v1/quotes → subscribe to symbols


2) High-Level Architecture

2.1 Overview

  • Order Service / OMS (Order Management System): validates orders and tracks their state.
  • Risk & Funds Service: checks margin and places a hold on funds (for buys) or shares (for sells).
  • Exchange Gateway: keeps persistent connections to the exchange using the FIX protocol (the standard trading message format), sends orders and receives fills.
  • Ledger / Positions: the record of cash and shares per user.
  • Market Data Service: receives the exchange price feed and streams quotes to users.
  • Kafka: the order events log, used by notifications, portfolio updates and audit.

2.2 Architecture Diagram

Architecture Diagram

flowchart LR
    U["Mobile / Web"] --> API["API Gateway"]
    API --> OMS["Order Service - state machine"]
    OMS --> RISK["Risk and Funds - holds"]
    RISK --> LED[("Ledger and Positions DB")]
    OMS --> DB[("Orders DB")]
    OMS --> EG["Exchange Gateway - FIX"]
    EG <--> EX["Stock Exchange"]
    EG -->|"fills, rejects"| OMS
    OMS --> K[("Order events - Kafka")]
    K --> NOTIF["Notifications"]
    K --> AUD["Audit store"]
    EX -->|"price feed"| MD["Market Data Service"]
    MD -->|"WebSocket quotes"| U

3) Data Model

CREATE TABLE orders (
  order_id      UUID PRIMARY KEY,
  user_id       UUID,
  symbol        TEXT,
  side          TEXT,        -- buy / sell
  type          TEXT,        -- market / limit / stop
  qty           INT,
  filled_qty    INT DEFAULT 0,
  limit_price   NUMERIC(12,2),
  status        TEXT,        -- pending, open, partially_filled, filled, cancelled, rejected
  exchange_order_id TEXT,
  version       INT
);
CREATE TABLE ledger_entries (   -- append-only money and share movements
  entry_id UUID, user_id UUID, asset TEXT,  -- 'INR' or a symbol
  amount NUMERIC, kind TEXT,   -- hold, release, trade, fee
  order_id UUID, created_at TIMESTAMP
);
CREATE TABLE fills (exec_id TEXT PRIMARY KEY, order_id UUID, qty INT, price NUMERIC, ts TIMESTAMP);
fills.exec_id is the exchange's unique execution ID. Using it as the key means a repeated fill message is ignored.

4) Key Flows

4.1 Placing a buy order

  1. The API checks the idempotency key, so a double tap does not create two orders.
  2. The Risk service checks available funds and places a hold of qty × limit price (plus fees). The money is now reserved.
  3. The OMS saves the order as pending and sends it to the exchange through the gateway.
  4. The exchange acknowledges → open.
  5. Fills arrive (maybe several partial fills). For each new exec_id: update filled_qty, turn the matching part of the hold into a real debit, add shares to positions, and publish an event.
  6. When fully filled → filled, and release any unused hold.

4.2 Cancel

Send a cancel to the exchange. The order is only cancelled when the exchange confirms, because a fill might arrive first. The rest of the hold is then released.


5) Deep Dive A — Correctness of money and state

  • State machine: only allowed transitions (e.g., filled can never go back to open). Use the version column (optimistic locking) so two updates don't overwrite each other.
  • Idempotency everywhere: client idempotency key on create, exec_id for fills, and our own order ID sent to the exchange as the client order ID.
  • Append-only ledger: balances are the sum of entries, and holds are entries too. Nothing is edited in place, which gives regulators a full history.
  • End-of-day reconciliation: compare our fills and positions with the exchange's trade file and the clearing house. Mismatches are investigated before the next day.


6) Deep Dive B — Market open and quotes

  • Spike at 9:15 AM: pre-scale before open, accept orders into a queue per exchange connection, and apply per-user rate limits. Orders placed before open ("AMO", after-market orders) are sent in a controlled stream.
  • Quotes to millions: the market data service receives the exchange feed once, then fans it out through many WebSocket servers. Users subscribe to specific symbols, and we send throttled updates (e.g., at most 4 per second per symbol per user), because the human eye can't use more.
  • In-memory OMS (LLD variant): keep open orders in memory, indexed by order ID and by symbol. Write every change to a log first so the OMS can recover after a crash by replaying it.


7) Trade-offs & Alternatives

DecisionChoiceWhyAlternative
Funds checkHold before sending the orderCan't overspendCheck after fill: risky
Order storeSQL with strict state machineCorrectness, auditNoSQL: scales easily, weaker transactions
Fill handlingDedupe by exec_idSafe against repeatsTrust the exchange never repeats: unsafe
QuotesThrottled WebSocket fan-outScales to millionsPush every tick: wastes bandwidth

8) Common Follow-up Questions

  • "What if the gateway loses connection mid-order?" The FIX protocol has sequence numbers. On reconnect, both sides replay missed messages. Until confirmed, the order stays pending.
  • "How are stop-loss orders handled?" Either the exchange supports them natively, or our service watches prices and places a market order when the trigger price is hit.
  • "Portfolio P&L?" Positions × latest price. Compute it on the client from streamed prices to avoid heavy server work.


9) Wrap-Up

Validate, hold funds, then send orders to the exchange through a FIX gateway. Track each order with a strict state machine, apply fills idempotently by execution ID to an append-only ledger, and reconcile with the exchange every day. Handle market-open spikes with pre-scaling and queues, and stream throttled quotes over WebSockets.

More Case Studies

Practice with a Mock Interview

Apply what you learned in a live system design mock interview with our AI interviewer.

Start System Design Interview →