Low-Level Design: Stock Trading Platform — Order Book, Matching Engine, and Portfolio Management

Core Entities

Order: order_id, user_id, symbol, side (BUY, SELL), order_type (MARKET, LIMIT, STOP_LIMIT, TRAILING_STOP), quantity, limit_price, stop_price, status (PENDING, PARTIALLY_FILLED, FILLED, CANCELLED, REJECTED), filled_quantity, average_fill_price, created_at. Trade: trade_id, buy_order_id, sell_order_id, symbol, quantity, price, executed_at. Position: position_id, user_id, symbol, quantity, average_cost_cents, current_value_cents, unrealized_pnl. Quote: symbol, bid_price, ask_price, last_price, volume, timestamp.

Order Book Data Structure

The order book maintains all outstanding limit orders sorted by price. Buy orders (bids): sorted descending by price (highest bid first). Sell orders (asks): sorted ascending by price (lowest ask first). Within each price level: sorted by time (FIFO — earlier orders execute first). Data structure: a sorted map (TreeMap in Java, SortedDict in Python) keyed by price, with each price level holding a queue of orders. Best bid: the maximum price in the buy side. Best ask: the minimum price in the sell side. Spread = best_ask – best_bid. Depth: the order book shows quantity available at each price level (L2 market data).

Matching Engine

The matching engine executes trades when buy and sell orders are compatible. Price-time priority: orders are matched first by price (best price executes first), then by time (older orders at the same price execute first). Market order matching: a market buy executes against the lowest-priced sell orders until filled. Limit order matching: a limit buy at price P executes against sell orders with price = best ask, or sell price <= best bid). (2) While crossed and quantity remaining: take the best opposing order. Create a Trade. Reduce both orders quantities. (3) If the incoming order is not fully filled: add the remainder to the book. Matching must be single-threaded or serialized per symbol to maintain price-time priority consistency.

def match_order(book, incoming):
    trades = []
    if incoming.side == BUY:
        while incoming.qty > 0 and book.asks and book.asks.min_price()  0:  # add remainder to book
        book.bids.add(incoming)
    return trades

Order Types

Market order: execute immediately at the best available price. Risk: in low-liquidity markets, slippage can be severe. Limit order: execute only at price = limit (sell). May not execute if the market never reaches the limit price. Stop order: becomes a market order when the price crosses the stop level. Stop-limit order: becomes a limit order at the stop level. Use case: stop-loss to limit downside. Trailing stop: the stop level follows the market price by a fixed delta (e.g., stop at current_price – $5). On price rise: stop rises. On price fall: stop holds, triggering when the price falls $5 from the peak. Good-till-cancelled (GTC): the order remains active until explicitly cancelled (vs day orders that expire at market close).

Portfolio Management and Risk

Portfolio tracks positions: each (user, symbol) pair has quantity and average cost basis. On each trade: update position quantity and compute new average cost basis: avg_cost = (old_qty * old_avg_cost + new_qty * fill_price) / (old_qty + new_qty). Unrealized PnL = (current_market_price – avg_cost) * quantity. Realized PnL is recorded on each closing trade. Risk checks before order acceptance: (1) Buying power: cash_balance >= order_quantity * current_price (for cash accounts). (2) Position limits: no single position > X% of portfolio (configurable). (3) Pattern day trader rules (US): accounts with less than $25K equity flagged after 3 day trades in 5 days. (4) Short selling eligibility: verify the user has a margin account and the stock is shortable.

Settlement

T+1 settlement (US equities since 2024): trades settle one business day after execution. Settlement means: cash is deducted from the buyer account and added to the seller, shares are transferred in the custodian system. During the T+1 window: shares and cash are in a pending state (cannot be withdrawn). Failed settlement: if a seller cannot deliver the shares by T+1, the broker buys the shares in the open market (buy-in) at the seller expense. Failed settlement tracking: monitor fail-to-deliver rates as a compliance KPI. Clearing: trades are netted at the clearinghouse (DTCC in the US) — if a user buys 100 shares and sells 80 shares of the same stock on the same day, only the net 20 shares are settled.

{
“@context”: “https://schema.org”,
“@type”: “FAQPage”,
“mainEntity”: [
{
“@type”: “Question”,
“name”: “How does price-time priority work in an order book matching engine?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “Price-time priority (pro-rata is the alternative) means: (1) Price priority: orders at better prices execute first. For buys: higher price executes first (more aggressive). For sells: lower price executes first. (2) Time priority: among orders at the same price level, the order that arrived first executes first (FIFO). This rewards aggressive pricing and early order submission. Implementation: the order book maintains a sorted structure (TreeMap/SortedDict) keyed by price. Each price level holds a queue of orders (deque). On match: pop from the front of the best price level queue. When a price level is empty: remove it from the sorted structure. The matching engine is single-threaded per symbol to maintain strict time ordering — no two orders at the same price level can be matched simultaneously.”
}
},
{
“@type”: “Question”,
“name”: “How do you handle partial fills in an order matching engine?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “A partial fill occurs when an incoming order is larger than the best opposing order. Example: buy 100 shares, best ask is 30 shares at $10. Match: trade 30 shares at $10. The buy order still has 70 shares remaining. Continue matching: take the next best ask (e.g., 50 shares at $10.01). Trade 50 shares. Buy order has 20 remaining. If no more asks are available within the limit price: add the remaining 20 shares to the buy side of the book as a resting limit order. Update the buy order status to PARTIALLY_FILLED with filled_quantity=80 and average_fill_price=(30*10 + 50*10.01)/80. Each partial fill generates a separate Trade record. The order status transitions from PENDING -> PARTIALLY_FILLED -> FILLED only when all quantity is executed.”
}
},
{
“@type”: “Question”,
“name”: “How do stop-loss orders work in a trading system?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “A stop-loss order becomes active when the market price crosses the stop level. Types: Stop Market: when the last trade price <= stop_price (for a long position), submit a market sell order. Executes immediately at the best available price. Risk: in fast markets, the execution price may be significantly worse than the stop (slippage). Stop Limit: when the price crosses the stop level, submit a limit sell order at limit_price. Guarantees the execution price but may not execute if the market falls through the limit. Implementation: maintain a trigger list sorted by stop price. On each trade execution, check if any stop orders should trigger (last_price = stop_price for buys). Trigger matching runs after each trade. Trailing stops: adjust the stop price dynamically — on each new high (for long positions), trail_stop = new_high – trail_delta.”
}
},
{
“@type”: “Question”,
“name”: “How does trade settlement work and why is T+1 important?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “Settlement is the transfer of shares and cash between buyer and seller to finalize a trade. T+1 (trade date plus 1 business day) is the current US equity settlement cycle (reduced from T+2 in May 2024). During the T+1 window: the buyer has not yet received the shares; the seller has not yet received cash. Both are in a pending state managed by the clearinghouse (DTCC in the US). Why T+1 matters for system design: (1) Risk exposure: during the settlement window, either party could default. Shorter settlement reduces counterparty risk. (2) Margin accounts: unsettled funds from a sale cannot be used to buy another security until settlement (pattern day trader rules in cash accounts). (3) Failed settlement: if a seller cannot deliver shares by T+1, the broker initiates a buy-in. Track settlement status per trade and alert on approaching settlement failures.”
}
},
{
“@type”: “Question”,
“name”: “How do you implement portfolio margin and buying power calculation?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “Buying power is the maximum order value a user can submit. Cash account: buying_power = settled_cash_balance (unsettled proceeds not available until T+1). Margin account: buying_power = (equity * margin_rate) + settled_cash. Standard Reg T margin: 2x buying power (50% initial margin requirement). Portfolio margin (for large accounts): uses a risk-based model (margin = max loss under a set of stress scenarios) — can give 4-6x leverage for hedged portfolios. Real-time calculation: on each order submission, compute the required margin: long order: required = quantity * current_price * initial_margin_rate. Short order: required = quantity * current_price * short_margin_rate (typically 150%). If required > available_buying_power: reject the order with INSUFFICIENT_BUYING_POWER. Re-check on order execution since the price may have moved. Maintenance margin check runs continuously; if equity falls below maintenance, issue a margin call.”
}
}
]
}

Asked at: Coinbase Interview Guide

Asked at: Stripe Interview Guide

Asked at: Databricks Interview Guide

Asked at: Shopify Interview Guide

Scroll to Top