Skip to content
Unverified — AI-generated content. Help verify this page

Design a Stock Exchange — Order Matching Engine

1. Problem Statement & Requirements

Functional Requirements

#RequirementDetails
FR-1Order PlacementSubmit market, limit, and stop orders
FR-2Order BookMaintain sorted buy/sell order book per security
FR-3Matching EngineMatch buy and sell orders by price-time priority
FR-4Real-Time Price FeedStream live price updates via WebSocket
FR-5Order ManagementCancel, modify existing orders
FR-6Trade ExecutionGenerate trade records when orders match
FR-7Account ManagementTrack positions, balances, margin
FR-8Market DataHistorical OHLCV, order book depth, last trade
FR-9Risk ManagementPre-trade risk checks, circuit breakers
FR-10SettlementT+1/T+2 trade settlement with clearinghouse

Non-Functional Requirements

#RequirementTarget
NFR-1LatencyOrder-to-ack < 10 microseconds (matching engine)
NFR-2Throughput1M+ orders/second
NFR-3Availability99.999% during market hours
NFR-4ConsistencyStrict ordering — every order processed exactly once, in sequence
NFR-5DeterminismSame input sequence always produces same output
NFR-6AuditabilityComplete audit trail for regulatory compliance

2. Back-of-Envelope Estimation

Scale Parameters

Listed Securities=10,000Active Traders=5M

Order Volume

NYSE daily volume3B shares/day in 6.5 market hoursOrders/Day5B (including cancels, many cancel before fill)Peak Orders/Second=5B23,400×51,068,376 orders/s

Matching Engine Latency Budget

Total Budget<10μs
StepBudget
Network receive1 μs
Parse + validate2 μs
Risk check1 μs
Match (order book lookup)3 μs
Generate execution reports2 μs
Network send1 μs

Market Data

Price Updates/Second=10,000 securities×100 updates/s=1M updates/sWebSocket Subscribers=500,000 concurrent connectionsFanout=1M×500K=500B messages/s (filtered by subscription)

Storage

Trade Records/Day=200MRecord Size=200 BDaily Trade Storage=200M×200 B=40 GB/dayOrder Audit Log=5B×300 B=1.5 TB/day

3. High-Level Design

Architecture Diagram

API Design

typescript
// Order APIs (FIX protocol or REST)
POST /api/v1/orders
     // Body: { symbol, side: "buy"|"sell", type: "market"|"limit"|"stop"|"stop_limit",
     //         quantity, price?, stopPrice?, timeInForce: "GTC"|"IOC"|"FOK"|"DAY" }
DELETE /api/v1/orders/{orderId}
PUT    /api/v1/orders/{orderId}
       // Body: { quantity?, price? }
GET    /api/v1/orders?status={open|filled|cancelled}

// Market Data APIs
GET /api/v1/market-data/{symbol}/quote
    // Returns: { bid, ask, last, volume, open, high, low, close }
GET /api/v1/market-data/{symbol}/orderbook?depth=10
    // Returns: { bids: [{price, qty}], asks: [{price, qty}] }
GET /api/v1/market-data/{symbol}/trades?limit=100
GET /api/v1/market-data/{symbol}/ohlcv?interval=1m&from={ts}&to={ts}

// WebSocket
WS  /api/v1/ws/market-data
    // Subscribe: { "action": "subscribe", "channels": ["trades.AAPL", "orderbook.AAPL"] }

// Account APIs
GET /api/v1/accounts/{accountId}/positions
GET /api/v1/accounts/{accountId}/balances
GET /api/v1/accounts/{accountId}/trades?from={date}&to={date}

4. Database Schema

Orders Table

sql
CREATE TABLE orders (
    order_id            BIGINT PRIMARY KEY,
    client_order_id     VARCHAR(50) NOT NULL,
    account_id          BIGINT NOT NULL,
    symbol              VARCHAR(10) NOT NULL,
    side                CHAR(1) NOT NULL,     -- 'B' (buy) or 'S' (sell)
    order_type          VARCHAR(10) NOT NULL,  -- MARKET, LIMIT, STOP, STOP_LIMIT
    quantity            BIGINT NOT NULL,       -- In shares (integer, no decimals)
    price               BIGINT,               -- In cents (integer arithmetic)
    stop_price          BIGINT,               -- Trigger price for stop orders
    time_in_force       VARCHAR(3) NOT NULL,   -- GTC, IOC, FOK, DAY
    filled_quantity     BIGINT DEFAULT 0,
    avg_fill_price      BIGINT DEFAULT 0,
    status              VARCHAR(15) NOT NULL,  -- NEW, PARTIAL, FILLED, CANCELLED, REJECTED
    sequence_number     BIGINT NOT NULL,       -- Sequencer-assigned
    received_at         TIMESTAMP(6) NOT NULL, -- Microsecond precision
    updated_at          TIMESTAMP(6) NOT NULL,
    INDEX idx_orders_symbol (symbol, status),
    INDEX idx_orders_account (account_id, status)
) PARTITION BY RANGE (received_at);

Trades Table

sql
CREATE TABLE trades (
    trade_id            BIGINT PRIMARY KEY,
    symbol              VARCHAR(10) NOT NULL,
    buy_order_id        BIGINT NOT NULL,
    sell_order_id       BIGINT NOT NULL,
    buyer_account_id    BIGINT NOT NULL,
    seller_account_id   BIGINT NOT NULL,
    quantity            BIGINT NOT NULL,
    price               BIGINT NOT NULL,      -- In cents
    executed_at         TIMESTAMP(6) NOT NULL,
    sequence_number     BIGINT NOT NULL,
    settlement_date     DATE NOT NULL,         -- T+1 or T+2
    settlement_status   VARCHAR(10) DEFAULT 'PENDING',
    INDEX idx_trades_symbol (symbol, executed_at),
    INDEX idx_trades_account (buyer_account_id, executed_at),
    INDEX idx_trades_settlement (settlement_date, settlement_status)
) PARTITION BY RANGE (executed_at);

Account Positions

sql
CREATE TABLE positions (
    account_id          BIGINT NOT NULL,
    symbol              VARCHAR(10) NOT NULL,
    quantity            BIGINT NOT NULL,       -- Positive = long, negative = short
    avg_cost            BIGINT NOT NULL,       -- Average cost basis in cents
    market_value        BIGINT NOT NULL,
    unrealized_pnl      BIGINT NOT NULL,
    realized_pnl        BIGINT NOT NULL,
    updated_at          TIMESTAMP(6) NOT NULL,
    PRIMARY KEY (account_id, symbol)
);

Order Audit Log (Append-Only)

sql
CREATE TABLE order_audit_log (
    sequence_number     BIGINT NOT NULL,
    event_type          VARCHAR(20) NOT NULL,
    -- NEW, MODIFY, CANCEL, FILL, PARTIAL_FILL, REJECT
    order_id            BIGINT NOT NULL,
    symbol              VARCHAR(10) NOT NULL,
    details             JSONB NOT NULL,
    timestamp_us        BIGINT NOT NULL,       -- Microseconds since epoch
    PRIMARY KEY (sequence_number)
);

5. Detailed Component Design

5.1 Order Book Data Structure

The order book is the central data structure — it must support O(1) best-price lookup and O(log n) insertion.

typescript
// Price levels use sorted tree (red-black tree) for O(log n) operations
// Orders within a price level use FIFO queue (doubly-linked list) for O(1) add/remove

interface Order {
  orderId: number;
  accountId: number;
  side: 'B' | 'S';
  price: number;          // In cents (integer arithmetic, no floating point!)
  quantity: number;
  remainingQty: number;
  timestamp: bigint;       // Nanosecond precision
  sequenceNumber: number;
}

interface PriceLevel {
  price: number;
  totalQuantity: number;
  orderCount: number;
  orders: DoublyLinkedList<Order>; // FIFO queue
}

class OrderBook {
  private bids: RedBlackTree<number, PriceLevel>;  // Sorted descending (highest first)
  private asks: RedBlackTree<number, PriceLevel>;   // Sorted ascending (lowest first)
  private orderMap: Map<number, Order>;              // orderId -> Order (for O(1) cancel)

  constructor(public readonly symbol: string) {
    this.bids = new RedBlackTree((a, b) => b - a); // Descending
    this.asks = new RedBlackTree((a, b) => a - b); // Ascending
    this.orderMap = new Map();
  }

  getBestBid(): PriceLevel | null {
    return this.bids.first();
  }

  getBestAsk(): PriceLevel | null {
    return this.asks.first();
  }

  getSpread(): number {
    const bestBid = this.getBestBid();
    const bestAsk = this.getBestAsk();
    if (!bestBid || !bestAsk) return Infinity;
    return bestAsk.price - bestBid.price;
  }

  addOrder(order: Order): void {
    const tree = order.side === 'B' ? this.bids : this.asks;

    let level = tree.get(order.price);
    if (!level) {
      level = {
        price: order.price,
        totalQuantity: 0,
        orderCount: 0,
        orders: new DoublyLinkedList(),
      };
      tree.insert(order.price, level);
    }

    level.orders.append(order);
    level.totalQuantity += order.remainingQty;
    level.orderCount++;
    this.orderMap.set(order.orderId, order);
  }

  removeOrder(orderId: number): Order | null {
    const order = this.orderMap.get(orderId);
    if (!order) return null;

    const tree = order.side === 'B' ? this.bids : this.asks;
    const level = tree.get(order.price);

    if (level) {
      level.orders.remove(order);
      level.totalQuantity -= order.remainingQty;
      level.orderCount--;

      // Remove empty price level
      if (level.orderCount === 0) {
        tree.remove(order.price);
      }
    }

    this.orderMap.delete(orderId);
    return order;
  }

  // Get order book depth (top N levels)
  getDepth(levels: number): OrderBookSnapshot {
    return {
      bids: this.getLevels(this.bids, levels),
      asks: this.getLevels(this.asks, levels),
    };
  }

  private getLevels(tree: RedBlackTree<number, PriceLevel>, n: number): DepthLevel[] {
    const result: DepthLevel[] = [];
    for (const [price, level] of tree.entries()) {
      if (result.length >= n) break;
      result.push({
        price,
        quantity: level.totalQuantity,
        orderCount: level.orderCount,
      });
    }
    return result;
  }
}

5.2 Matching Engine

The matching engine is the heart of the exchange — it must be deterministic, fast, and correct.

typescript
class MatchingEngine {
  private orderBooks: Map<string, OrderBook> = new Map();
  private sequenceNumber: number = 0;
  private stopOrders: Map<string, StopOrder[]> = new Map();

  processOrder(incomingOrder: IncomingOrder): ExecutionReport[] {
    const book = this.getOrCreateOrderBook(incomingOrder.symbol);
    const reports: ExecutionReport[] = [];

    // Acknowledge receipt
    reports.push(this.createAck(incomingOrder));

    switch (incomingOrder.orderType) {
      case 'MARKET':
        this.matchMarketOrder(book, incomingOrder, reports);
        break;
      case 'LIMIT':
        this.matchLimitOrder(book, incomingOrder, reports);
        break;
      case 'STOP':
        this.registerStopOrder(incomingOrder);
        break;
      case 'STOP_LIMIT':
        this.registerStopLimitOrder(incomingOrder);
        break;
    }

    // Check if any stop orders are triggered
    this.checkStopOrders(book, reports);

    return reports;
  }

  private matchLimitOrder(
    book: OrderBook,
    order: IncomingOrder,
    reports: ExecutionReport[]
  ): void {
    const oppositeSide = order.side === 'B' ? book.asks : book.bids;
    let remainingQty = order.quantity;

    // Match against opposite side of the book
    while (remainingQty > 0) {
      const bestLevel = order.side === 'B' ? book.getBestAsk() : book.getBestBid();

      if (!bestLevel) break; // No orders on opposite side

      // Price check: can we match?
      if (order.side === 'B' && bestLevel.price > order.price) break;  // Ask too high
      if (order.side === 'S' && bestLevel.price < order.price) break;  // Bid too low

      // Match against orders at this price level (FIFO)
      const levelOrders = bestLevel.orders.toArray();
      for (const restingOrder of levelOrders) {
        if (remainingQty <= 0) break;

        const fillQty = Math.min(remainingQty, restingOrder.remainingQty);
        const fillPrice = restingOrder.price; // Price-time priority: resting order's price

        // Generate trade
        const trade = this.createTrade(
          order, restingOrder, fillQty, fillPrice
        );
        reports.push(trade);

        // Update quantities
        remainingQty -= fillQty;
        restingOrder.remainingQty -= fillQty;

        // Remove fully filled resting order
        if (restingOrder.remainingQty === 0) {
          book.removeOrder(restingOrder.orderId);
          reports.push(this.createFillReport(restingOrder, 'FILLED'));
        } else {
          reports.push(this.createFillReport(restingOrder, 'PARTIAL'));
        }
      }
    }

    // Handle remaining quantity based on time-in-force
    if (remainingQty > 0) {
      switch (order.timeInForce) {
        case 'IOC': // Immediate or Cancel - cancel remainder
          reports.push(this.createCancelReport(order, remainingQty, 'IOC_EXPIRED'));
          break;
        case 'FOK': // Fill or Kill - should have been checked before matching
          // If we are here with remaining, we need to roll back (FOK check done upfront)
          break;
        case 'GTC': // Good Till Cancel - add to book
        case 'DAY': // Good for day - add to book
          const bookOrder: Order = {
            orderId: order.orderId,
            accountId: order.accountId,
            side: order.side,
            price: order.price,
            quantity: order.quantity,
            remainingQty: remainingQty,
            timestamp: BigInt(Date.now()) * 1000000n,
            sequenceNumber: this.sequenceNumber++,
          };
          book.addOrder(bookOrder);
          reports.push(this.createRestingReport(order, remainingQty));
          break;
      }
    } else {
      reports.push(this.createFillReport(order, 'FILLED'));
    }
  }

  private matchMarketOrder(
    book: OrderBook,
    order: IncomingOrder,
    reports: ExecutionReport[]
  ): void {
    // Market order: match at any price, no remainder goes to book
    const limitOrder = { ...order, price: order.side === 'B' ? Infinity : 0 };
    this.matchLimitOrder(book, limitOrder, reports);

    // If not fully filled, cancel remainder (market orders do not rest)
    // Already handled by time-in-force logic
  }

  private checkStopOrders(book: OrderBook, reports: ExecutionReport[]): void {
    const symbol = book.symbol;
    const stops = this.stopOrders.get(symbol) ?? [];
    const lastTradePrice = this.getLastTradePrice(symbol);

    const triggered: StopOrder[] = [];

    for (const stop of stops) {
      if (
        (stop.side === 'B' && lastTradePrice >= stop.stopPrice) ||
        (stop.side === 'S' && lastTradePrice <= stop.stopPrice)
      ) {
        triggered.push(stop);
      }
    }

    // Remove triggered stops and convert to market/limit orders
    for (const stop of triggered) {
      const idx = stops.indexOf(stop);
      stops.splice(idx, 1);

      if (stop.orderType === 'STOP') {
        this.matchMarketOrder(book, { ...stop, orderType: 'MARKET' }, reports);
      } else {
        this.matchLimitOrder(book, { ...stop, orderType: 'LIMIT' }, reports);
      }
    }
  }

  private createTrade(
    aggressor: IncomingOrder,
    resting: Order,
    quantity: number,
    price: number
  ): ExecutionReport {
    return {
      type: 'TRADE',
      tradeId: this.sequenceNumber++,
      symbol: aggressor.symbol,
      buyOrderId: aggressor.side === 'B' ? aggressor.orderId : resting.orderId,
      sellOrderId: aggressor.side === 'S' ? aggressor.orderId : resting.orderId,
      quantity,
      price,
      timestamp: process.hrtime.bigint(),
    };
  }
}

5.3 Sequencer (Total Ordering)

All orders must be processed in a deterministic, totally ordered sequence.

typescript
class Sequencer {
  private sequence: bigint = 0n;
  private journal: AppendOnlyLog;
  private replicas: SequencerReplica[];

  async processMessage(message: RawOrder): Promise<SequencedOrder> {
    // Assign monotonically increasing sequence number
    const seqNum = this.sequence++;

    const sequenced: SequencedOrder = {
      ...message,
      sequenceNumber: seqNum,
      timestamp: process.hrtime.bigint(), // Nanosecond precision
    };

    // Write to journal (append-only, durable)
    await this.journal.append(sequenced);

    // Replicate to followers (synchronous for durability)
    await Promise.all(
      this.replicas.map(r => r.replicate(sequenced))
    );

    // Route to correct matching engine based on symbol
    const partition = this.getPartition(message.symbol);
    await this.matchingEngines[partition].enqueue(sequenced);

    return sequenced;
  }

  // Leader election via Raft consensus
  async electLeader(): Promise<void> {
    // If leader fails, follower with most up-to-date journal becomes new leader
    // Recovery: replay journal from last checkpoint
  }
}

5.4 Real-Time Price Feed

typescript
class MarketDataService {
  private subscribers: Map<string, Set<WebSocket>> = new Map();
  private ohlcvBuckets: Map<string, Map<string, OHLCVBar>> = new Map(); // symbol -> interval -> bar

  // Handle new trade event from matching engine
  onTrade(trade: TradeEvent): void {
    // Update last trade
    this.publishToSubscribers(`trades.${trade.symbol}`, {
      type: 'trade',
      symbol: trade.symbol,
      price: trade.price,
      quantity: trade.quantity,
      timestamp: trade.timestamp,
      side: trade.aggressorSide,
    });

    // Update OHLCV bars
    this.updateOHLCV(trade);

    // Update ticker
    this.publishToSubscribers(`ticker.${trade.symbol}`, {
      type: 'ticker',
      symbol: trade.symbol,
      last: trade.price,
      bid: this.getBestBid(trade.symbol),
      ask: this.getBestAsk(trade.symbol),
      volume: this.getDailyVolume(trade.symbol),
      change: this.getDailyChange(trade.symbol),
    });
  }

  // Handle order book update
  onBookUpdate(symbol: string, book: OrderBookSnapshot): void {
    this.publishToSubscribers(`orderbook.${symbol}`, {
      type: 'orderbook',
      symbol,
      bids: book.bids.slice(0, 20), // Top 20 levels
      asks: book.asks.slice(0, 20),
      timestamp: Date.now(),
    });
  }

  private updateOHLCV(trade: TradeEvent): void {
    const intervals = ['1s', '1m', '5m', '15m', '1h', '4h', '1d'];

    for (const interval of intervals) {
      const bucketKey = this.getBucketKey(trade.timestamp, interval);
      const symbolBuckets = this.ohlcvBuckets.get(trade.symbol) ?? new Map();

      let bar = symbolBuckets.get(bucketKey);
      if (!bar) {
        bar = {
          open: trade.price,
          high: trade.price,
          low: trade.price,
          close: trade.price,
          volume: 0,
          timestamp: this.getBucketStart(trade.timestamp, interval),
        };
        symbolBuckets.set(bucketKey, bar);
        this.ohlcvBuckets.set(trade.symbol, symbolBuckets);
      }

      bar.high = Math.max(bar.high, trade.price);
      bar.low = Math.min(bar.low, trade.price);
      bar.close = trade.price;
      bar.volume += trade.quantity;
    }
  }

  // WebSocket subscription management
  handleSubscription(ws: WebSocket, channel: string, action: 'subscribe' | 'unsubscribe'): void {
    if (action === 'subscribe') {
      const subs = this.subscribers.get(channel) ?? new Set();
      subs.add(ws);
      this.subscribers.set(channel, subs);

      // Send initial snapshot
      if (channel.startsWith('orderbook.')) {
        const symbol = channel.split('.')[1];
        const snapshot = this.getCurrentSnapshot(symbol);
        ws.send(JSON.stringify({ type: 'snapshot', ...snapshot }));
      }
    } else {
      this.subscribers.get(channel)?.delete(ws);
    }
  }

  private publishToSubscribers(channel: string, data: object): void {
    const subs = this.subscribers.get(channel);
    if (!subs) return;

    const message = JSON.stringify(data);
    for (const ws of subs) {
      if (ws.readyState === WebSocket.OPEN) {
        ws.send(message);
      }
    }
  }
}

5.5 Risk Management

typescript
class RiskEngine {
  async preTradeCheck(order: IncomingOrder): Promise<RiskCheckResult> {
    const checks: RiskCheckResult[] = await Promise.all([
      this.checkOrderSize(order),
      this.checkPriceBand(order),
      this.checkAccountBalance(order),
      this.checkPositionLimit(order),
      this.checkRateLimit(order),
      this.checkCircuitBreaker(order.symbol),
    ]);

    const failed = checks.find(c => !c.passed);
    if (failed) {
      return { passed: false, reason: failed.reason };
    }

    return { passed: true };
  }

  private async checkPriceBand(order: IncomingOrder): Promise<RiskCheckResult> {
    if (order.orderType === 'MARKET') return { passed: true };

    const lastPrice = await this.getLastTradePrice(order.symbol);
    if (!lastPrice) return { passed: true }; // No reference price yet

    const deviation = Math.abs(order.price - lastPrice) / lastPrice;
    const maxDeviation = 0.10; // 10% from last trade

    if (deviation > maxDeviation) {
      return {
        passed: false,
        reason: `Price ${order.price} deviates ${(deviation * 100).toFixed(1)}% from last trade ${lastPrice}`,
      };
    }

    return { passed: true };
  }

  private async checkCircuitBreaker(symbol: string): Promise<RiskCheckResult> {
    const dayOpen = await this.getDayOpenPrice(symbol);
    const lastPrice = await this.getLastTradePrice(symbol);

    if (!dayOpen || !lastPrice) return { passed: true };

    const dayChange = (lastPrice - dayOpen) / dayOpen;

    // Level 1: 7% drop - 15 min halt
    // Level 2: 13% drop - 15 min halt
    // Level 3: 20% drop - halt for day
    if (Math.abs(dayChange) > 0.20) {
      return { passed: false, reason: `Circuit breaker Level 3: ${symbol} halted for day` };
    }
    if (Math.abs(dayChange) > 0.13) {
      return { passed: false, reason: `Circuit breaker Level 2: ${symbol} halted 15 min` };
    }
    if (Math.abs(dayChange) > 0.07) {
      return { passed: false, reason: `Circuit breaker Level 1: ${symbol} halted 15 min` };
    }

    return { passed: true };
  }

  private async checkAccountBalance(order: IncomingOrder): Promise<RiskCheckResult> {
    if (order.side === 'S') {
      // Sell: check if account has shares
      const position = await this.getPosition(order.accountId, order.symbol);
      if (position.quantity < order.quantity) {
        return { passed: false, reason: 'Insufficient shares' };
      }
    } else {
      // Buy: check if account has buying power
      const buyingPower = await this.getBuyingPower(order.accountId);
      const cost = order.orderType === 'MARKET'
        ? await this.estimateMarketOrderCost(order.symbol, order.quantity)
        : order.price * order.quantity;

      if (cost > buyingPower) {
        return { passed: false, reason: 'Insufficient buying power' };
      }
    }

    return { passed: true };
  }
}

5.6 Trade Settlement

typescript
class SettlementService {
  // Run daily at end of trading day
  async processDailySettlement(settlementDate: Date): Promise<SettlementReport> {
    // Get all trades due for settlement today
    const trades = await this.getTradesForSettlement(settlementDate);

    // Net positions per account per symbol (multilateral netting)
    const netPositions = this.calculateNetPositions(trades);

    // Validate all parties can settle
    const validationResults = await Promise.all(
      netPositions.map(np => this.validateSettlement(np))
    );

    const failed = validationResults.filter(v => !v.valid);
    if (failed.length > 0) {
      await this.handleSettlementFailures(failed);
    }

    // Execute settlement
    for (const position of netPositions) {
      await this.executeSettlement(position);
    }

    return {
      date: settlementDate,
      tradesSettled: trades.length,
      totalValue: trades.reduce((sum, t) => sum + t.price * t.quantity, 0),
      failures: failed.length,
    };
  }

  private calculateNetPositions(trades: Trade[]): NetPosition[] {
    // Multilateral netting reduces the number of actual transfers
    const netMap = new Map<string, Map<string, number>>(); // account -> symbol -> net qty

    for (const trade of trades) {
      // Buyer gets shares
      const buyerKey = `${trade.buyerAccountId}`;
      if (!netMap.has(buyerKey)) netMap.set(buyerKey, new Map());
      const buyerPositions = netMap.get(buyerKey)!;
      buyerPositions.set(
        trade.symbol,
        (buyerPositions.get(trade.symbol) ?? 0) + trade.quantity
      );

      // Seller delivers shares
      const sellerKey = `${trade.sellerAccountId}`;
      if (!netMap.has(sellerKey)) netMap.set(sellerKey, new Map());
      const sellerPositions = netMap.get(sellerKey)!;
      sellerPositions.set(
        trade.symbol,
        (sellerPositions.get(trade.symbol) ?? 0) - trade.quantity
      );
    }

    // Convert to array
    const positions: NetPosition[] = [];
    for (const [account, symbols] of netMap) {
      for (const [symbol, netQty] of symbols) {
        positions.push({ accountId: account, symbol, netQuantity: netQty });
      }
    }
    return positions;
  }
}

6. Scaling & Bottlenecks

What Breaks First

ComponentBottleneckSolution
Matching EngineSingle-threaded per symbolPartition by symbol, each symbol on dedicated core
SequencerSingle point of serializationLMAX Disruptor pattern, mechanical sympathy
Market Data1M updates/s x 500K subscribersTopic-based pub/sub, UDP multicast for co-located
Order GatewayNetwork I/OKernel bypass (DPDK), FPGA NICs
Audit Log1.5 TB/day writesAppend-only log with async replication
WebSocket500K connectionsHorizontal WebSocket server fleet

Low-Latency Techniques

typescript
// LMAX Disruptor-inspired ring buffer for lock-free messaging
class RingBuffer<T> {
  private buffer: T[];
  private writePos: number = 0;
  private readPos: number = 0;
  private readonly size: number;

  constructor(size: number) {
    // Size must be power of 2 for bitwise modulo
    this.size = size;
    this.buffer = new Array(size);
  }

  // Single writer, multiple readers
  publish(event: T): boolean {
    if (this.writePos - this.readPos >= this.size) {
      return false; // Buffer full
    }
    this.buffer[this.writePos & (this.size - 1)] = event;
    this.writePos++;
    return true;
  }

  consume(): T | null {
    if (this.readPos >= this.writePos) {
      return null; // No data
    }
    const event = this.buffer[this.readPos & (this.size - 1)];
    this.readPos++;
    return event;
  }
}

Key low-latency principles:

  1. No garbage collection pauses — Use pre-allocated object pools
  2. No system calls in hot path — Memory-mapped files, kernel bypass
  3. CPU affinity — Pin matching engine thread to specific CPU core
  4. Cache-friendly data structures — Arrays over linked lists, struct-of-arrays
  5. No locks — Lock-free data structures (CAS operations)
  6. Integer arithmetic — Prices in cents, no floating point
Floating point: 0.1+0.2=0.30000000000000004(WRONG for finance)Integer: 10+20=30 cents(CORRECT)

7. Trade-offs & Alternatives

Matching Engine Architecture

ApproachProCon
Single-threaded per symbolDeterministic, simpleLimited throughput per symbol
Multi-threaded per symbolHigher throughputComplex synchronization, non-deterministic
FPGA-basedSub-microsecond latencyExpensive, hard to update
GPU-basedMassive parallelismHigh latency per operation

Communication Protocol

ProtocolProConUse Case
FIXIndustry standard, reliableVerbose, text-basedInstitutional clients
Binary (SBE)Compact, fast parseNot human-readableInternal, co-located
WebSocketEasy integrationHigher latencyRetail clients
UDP MulticastLowest latencyUnreliable, LAN onlyMarket data (co-located)

State Replication

ApproachProCon
Raft ConsensusStrong consistencyLatency overhead
Primary-BackupSimple, fastManual failover
Event SourcingPerfect replayStorage heavy
LMAX-style (replicated journal)Deterministic replayComplex implementation

8. Advanced Topics

8.1 Order Types Deep Dive

Order TypeBehaviorUse Case
MarketExecute immediately at best availableUrgency, guaranteed fill
LimitExecute at specified price or betterPrice control
StopTrigger market order when price reachedStop loss
Stop-LimitTrigger limit order when price reachedControlled stop loss
IcebergShow only portion of total quantityHide large orders
TWAPExecute evenly over time periodMinimize market impact

8.2 Market Microstructure

Spread=Best AskBest BidMid Price=Best Bid+Best Ask2VWAP=iPi×QiiQi

8.3 Regulatory Compliance

  • MiFID II (EU): Report all trades within 15 minutes, best execution obligation
  • Reg NMS (US): National best bid/offer, trade-through protection
  • Market Abuse Regulation: Detect spoofing, layering, wash trading

8.4 Disaster Recovery

Two matching engine instances run simultaneously. The backup replays the same journal and can take over within milliseconds if the primary fails.


9. Interview Tips

Key Points to Emphasize

  1. The order book is the core data structure — Red-black tree of price levels, each with a FIFO queue of orders.
  2. Price-time priority — Orders match at the resting order's price; time priority within a price level.
  3. Determinism is non-negotiable — Same sequence of inputs must always produce same outputs.
  4. Integer arithmetic for prices — Never use floating point for financial calculations.
  5. Sequencer provides total ordering — All events flow through a single sequencer for deterministic replay.

Common Mistakes

  • Using floating point for prices — this is a critical error in financial systems.
  • Making the matching engine multi-threaded per symbol — it breaks determinism.
  • Not discussing the sequencer — total ordering is essential for correctness and recovery.
  • Treating market data as a simple database query — it needs real-time streaming at massive scale.
  • Forgetting settlement — the exchange is not done when a trade executes; settlement is days later.

Follow-Up Questions to Expect

  • How would you handle a flash crash? (Circuit breakers at security level and market-wide.)
  • How do you detect market manipulation (spoofing, layering)?
  • How would you add support for options/derivatives?
  • What happens when the primary matching engine fails? (Failover to hot standby replaying the same journal.)
  • How do you handle market open (all orders accumulated overnight execute at once)?

Time Allocation in 45-min Interview

PhaseTimeFocus
Requirements3 minOrder types, latency targets, throughput
High-Level Design8 minArchitecture, key components
Deep Dive: Order Book10 minData structure, price-time priority
Deep Dive: Matching10 minLimit order matching, edge cases
Deep Dive: Market Data7 minWebSocket feed, OHLCV aggregation
Risk & Compliance5 minPre-trade checks, circuit breakers
Q&A2 minSettlement, failover, latency optimization

"What I cannot create, I do not understand." — Richard Feynman