โ† 2026-06-12 ๐Ÿ“‚ All Days 2026-06-14 โ†’

๐Ÿ“ Review Day 65 โ€” Expert Phase

๐Ÿ“Š NeetCode: 54/150 ยท SysDesign: 53/40 ยท Behavioral: 53/40 ยท Python: 16 ยท AI: 26/30

๐Ÿ”ฅ 65-day streak!

Today is a Saturday Deep Dive day โ€” see 2026-06-13-deepdive.md for the full content.


Mini-Quiz: Days 61โ€“64

Q1: [System Design] What makes designing a Stock Exchange / Order Matching Engine different from a typical CRUD system?

A: Determinism and latency are paramount. The matching engine must be single-threaded per symbol for correctness (no race conditions), use event sourcing for crash recovery, and achieve microsecond-level latency. Unlike CRUD systems, you can't use a relational DB as the primary store โ€” state lives in-memory with a write-ahead log.

Q2: [Algorithms] In Binary Tree Level Order Traversal (BFS), what's the key to separating nodes by level?

A: Before processing each level, record queue.size(). Dequeue exactly that many nodes โ€” they all belong to the current level. Their children go into the queue for the next level. This avoids needing a sentinel/null marker.

Q3: [Python Craft] Why is the Singleton pattern often considered an anti-pattern in Python?

A: It creates hidden global state, makes testing hard (can't reset between tests), and violates dependency injection principles. Python alternatives: module-level globals (already singleton by nature), functools.lru_cache on a factory function, or dependency injection via constructor. Use Singleton only when you truly need one shared resource (e.g., a connection pool) and can't inject it.

๐Ÿ”ฌ Saturday Deep Dive: ่‚ก็ฅจไบคๆ˜“ๆ‰€ๆ’ฎๅˆๅผ•ๆ“Ž / Stock Exchange Order Matching Engine (18 min read)

๐Ÿ“Š NeetCode: 54/150 ยท SysDesign: 53/40 ยท Behavioral: 53/40 ยท Frontend: 37/50 ยท AI: 26/30

๐Ÿ”ฅ 65-day streak!


Overview / ๆฆ‚่ฟฐ

ไธญๆ–‡๏ผš

ๆ’ฎๅˆๅผ•ๆ“Ž๏ผˆOrder Matching Engine๏ผŒOME๏ผ‰ๆ˜ฏๆ•ดไธชไบคๆ˜“ๆ‰€็š„ๅฟƒ่„ใ€‚็บฝ็บฆ่ฏๅˆธไบคๆ˜“ๆ‰€ๆฏ็ง’ๅค„็†ๆ•ฐ็™พไธ‡็ฌ”่ฎขๅ•๏ผŒๅปถ่ฟŸ่ฆๆฑ‚ๅœจๅพฎ็ง’็บงๅˆซใ€‚ๅฎƒ่งฃๅ†ณ็š„ๆ ธๅฟƒ้—ฎ้ข˜ๆ˜ฏ๏ผšๅœจไปทๆ ผ/ๆ—ถ้—ดไผ˜ๅ…ˆ็š„ๅŽŸๅˆ™ไธ‹๏ผŒๅฐ†ไนฐๅ•๏ผˆBid๏ผ‰ๅ’Œๅ–ๅ•๏ผˆAsk๏ผ‰้ซ˜ๆ•ˆ้…ๅฏน๏ผŒๅนถไปฅๆœ€ๅ…ฌๅนณ็š„ไปทๆ ผๆˆไบคใ€‚

่ฟ™ไธช็ณป็ปŸ็š„้šพ็‚นไธๅœจไบŽ้€ป่พ‘ๅคๆ‚๏ผŒ่€ŒๅœจไบŽ๏ผš

  • ๆž็ซฏๅžๅ้‡๏ผšๆฏ็ง’็™พไธ‡็บง TPS
  • ็กฎๅฎšๆ€ง๏ผš็›ธๅŒ็š„่ฎขๅ•ๅบๅˆ—๏ผŒๆฐธ่ฟœไบง็”Ÿ็›ธๅŒ็š„ๆˆไบค็ป“ๆžœ
  • ๅ…ฌๅนณๆ€ง๏ผšFIFO๏ผˆๅ…ˆ่ฟ›ๅ…ˆๅ‡บ๏ผ‰ไฟ่ฏ๏ผŒๆ— "ๆ’้˜Ÿ"
  • ๆŒไน…ๆ€ง๏ผšๅดฉๆบƒๅŽ่ƒฝ็ฒพ็กฎ้‡ๆ”พ

English:

An order matching engine is the core of any exchange โ€” NYSE handles millions of orders/second with microsecond latency. The problem: given buy (bid) and sell (ask) orders, match them at the best fair price following price-time priority. Hard part isn't the algorithm โ€” it's building it at extreme throughput with deterministic, crash-safe execution.

Why this matters in interviews: it combines data structures (priority queues), concurrency, event sourcing, and system design into one question. Meta, Jane Street, Citadel, and Stripe all ask variants of this.


Part 1: Theory / ็†่ฎบๅŸบ็ก€ (5 min)

ๆ ธๅฟƒๆฆ‚ๅฟต / Core Concepts

่ฎขๅ•็ฑปๅž‹ / Order Types

้™ไปทๅ• (Limit Order):  ไปฅๆŒ‡ๅฎšไปทๆ ผๆˆ–ๆ›ดไผ˜ไปทๆ ผไนฐ/ๅ–
ๅธ‚ไปทๅ• (Market Order): ็ซ‹ๅณไปฅๆœ€ไผ˜ๅธ‚ไปทๆˆไบค
ๆ’คๅ•   (Cancel Order): ๅ–ๆถˆๅทฒๆœ‰ๆŒ‚ๅ•

่ฎขๅ•็ฐฟ็ป“ๆž„ / The Order Book

ASKS (ๅ–ๆ–น) โ€” ๆŒ‰ไปทๆ ผๅ‡ๅบๆŽ’ๅˆ— โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ Price | Qty | Orders โ”‚ โ”‚ 102.50 | 500 | [A1, A2] โ”‚ โ† best ask (lowest sell) โ”‚ 103.00 | 300 | [A3] โ”‚ โ”‚ 103.50 | 1000 | [A4, A5] โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ SPREAD (ๅทฎไปท) โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ 101.50 | 400 | [B3, B4] โ”‚ โ”‚ 101.00 | 600 | [B2] โ”‚ โ”‚ 100.50 | 800 | [B1] โ”‚ โ† best bid (highest buy) โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ BIDS (ไนฐๆ–น) โ€” ๆŒ‰ไปทๆ ผ้™ๅบๆŽ’ๅˆ—

ๆ’ฎๅˆ่ง„ๅˆ™ / Matching Rules

  1. Price Priority: Best price first (highest bid, lowest ask)
  2. Time Priority: Same price โ†’ earliest order first (FIFO)
  3. ๆˆไบคๆกไปถ: bid_price >= ask_price ๆ—ถ่งฆๅ‘ๆˆไบค

ไธ‰ไธชไธๅ˜้‡ / Three Invariants

  • ไปปๆ„ๆ—ถๅˆป๏ผŒbest_bid < best_ask๏ผˆๅฆๅˆ™็ซ‹ๅณๆ’ฎๅˆ๏ผ‰
  • ๅŒไปทๆ ผ็บงๅˆซๅ†…๏ผŒ่ฎขๅ•ไธฅๆ ผ FIFO
  • ๆ‰€ๆœ‰็Šถๆ€ๅ˜ๆ›ด้€š่ฟ‡ event log ๆŒไน…ๅŒ–๏ผŒๅฏ้‡ๆ”พ

ๅ…ณ้”ฎๆ•ฐๆฎ็ป“ๆž„ / Key Data Structures

# Price level: all orders at the same price
# Use deque for O(1) FIFO add/remove at head
from collections import deque

class PriceLevel:
    def __init__(self, price: float):
        self.price = price
        self.orders: deque[Order] = deque()  # FIFO queue
        self.total_qty: int = 0

# Order book side: sorted price levels
# Bids: max-heap (negate price for Python's min-heap)
# Asks: min-heap
import heapq

class OrderBookSide:
    def __init__(self, is_bid: bool):
        self.is_bid = is_bid
        self.levels: dict[float, PriceLevel] = {}
        self.heap: list[float] = []  # price heap for O(log n) best price

Part 2: Step-by-Step Implementation / ไธ€ๆญฅไธ€ๆญฅๅฎž็Žฐ (8 min)

ๅฎŒๆ•ดๆ’ฎๅˆๅผ•ๆ“Žๅฎž็Žฐ / Full Matching Engine

import heapq
import time
import uuid
from collections import deque
from dataclasses import dataclass, field
from enum import Enum
from typing import Optional


class Side(Enum):
    BID = "bid"  # Buy order
    ASK = "ask"  # Sell order


class OrderType(Enum):
    LIMIT = "limit"
    MARKET = "market"


@dataclass
class Order:
    order_id: str
    symbol: str
    side: Side
    order_type: OrderType
    price: Optional[float]   # None for market orders
    quantity: int
    timestamp: int = field(default_factory=lambda: time.time_ns())
    filled_qty: int = 0

    @property
    def remaining_qty(self) -> int:
        return self.quantity - self.filled_qty


@dataclass
class Trade:
    trade_id: str
    symbol: str
    price: float
    quantity: int
    buyer_order_id: str
    seller_order_id: str
    timestamp: int = field(default_factory=lambda: time.time_ns())


class OrderBook:
    """
    Maintains bids and asks for a single symbol.

    Internal representation:
      - bids: max-heap (negate price โ†’ Python min-heap trick)
      - asks: min-heap
      - levels: dict mapping price โ†’ PriceLevel (deque of orders)
      - order_map: dict mapping order_id โ†’ Order (for O(1) cancel)
    """

    def __init__(self, symbol: str):
        self.symbol = symbol
        # Bids: store (-price, timestamp) for max-heap behavior
        self._bid_heap: list[tuple[float, int, float]] = []  # (-price, ts, price)
        # Asks: store (price, timestamp)
        self._ask_heap: list[tuple[float, int]] = []
        self._bid_levels: dict[float, deque] = {}
        self._ask_levels: dict[float, deque] = {}
        self._order_map: dict[str, Order] = {}  # for cancellation
        self._cancelled: set[str] = set()       # lazy deletion set

    def best_bid(self) -> Optional[float]:
        """O(log n) โ€” skip cancelled/empty levels"""
        while self._bid_heap:
            neg_price, _, price = self._bid_heap[0]
            level = self._bid_levels.get(price)
            # Clean up: if level is empty or all cancelled, pop and continue
            if level and len(level) > 0:
                return price
            heapq.heappop(self._bid_heap)
            if price in self._bid_levels:
                del self._bid_levels[price]
        return None

    def best_ask(self) -> Optional[float]:
        """O(log n) โ€” skip empty levels"""
        while self._ask_heap:
            price, _ = self._ask_heap[0]
            level = self._ask_levels.get(price)
            if level and len(level) > 0:
                return price
            heapq.heappop(self._ask_heap)
            if price in self._ask_levels:
                del self._ask_levels[price]
        return None

    def add_limit_order(self, order: Order):
        """Add limit order to the book (no immediate matching here)."""
        self._order_map[order.order_id] = order
        if order.side == Side.BID:
            if order.price not in self._bid_levels:
                self._bid_levels[order.price] = deque()
                # Push to heap: negate for max-heap, include timestamp for tie-breaking
                heapq.heappush(self._bid_heap, (-order.price, order.timestamp, order.price))
            self._bid_levels[order.price].append(order)
        else:
            if order.price not in self._ask_levels:
                self._ask_levels[order.price] = deque()
                heapq.heappush(self._ask_heap, (order.price, order.timestamp))
            self._ask_levels[order.price].append(order)

    def cancel_order(self, order_id: str) -> bool:
        """
        Lazy deletion: mark as cancelled, skip during matching.
        O(1) โ€” no heap restructuring needed.
        """
        if order_id not in self._order_map:
            return False
        self._cancelled.add(order_id)
        return True

    def pop_best_ask(self) -> Optional[Order]:
        """Get and remove the best ask order (FIFO within price level)."""
        while True:
            best = self.best_ask()
            if best is None:
                return None
            level = self._ask_levels[best]
            while level:
                order = level[0]
                if order.order_id in self._cancelled or order.remaining_qty <= 0:
                    level.popleft()  # Remove cancelled/filled orders
                    continue
                return level[0]  # Peek โ€” don't pop yet (partial fills)
            # Level empty after cleanup
            del self._ask_levels[best]
            heapq.heappop(self._ask_heap)

    def pop_best_bid(self) -> Optional[Order]:
        """Get and remove the best bid order (FIFO within price level)."""
        while True:
            best = self.best_bid()
            if best is None:
                return None
            level = self._bid_levels[best]
            while level:
                order = level[0]
                if order.order_id in self._cancelled or order.remaining_qty <= 0:
                    level.popleft()
                    continue
                return level[0]
            del self._bid_levels[best]
            heapq.heappop(self._bid_heap)


class MatchingEngine:
    """
    Core matching engine. Single-threaded per symbol (no locks needed).

    Architecture:
      - One OrderBook per symbol (isolated state)
      - process_order() = the single entry point
      - Returns list of Trade objects (for downstream processing)

    Matching algorithm: continuous auction (price-time priority)
    """

    def __init__(self):
        self._books: dict[str, OrderBook] = {}

    def _get_book(self, symbol: str) -> OrderBook:
        if symbol not in self._books:
            self._books[symbol] = OrderBook(symbol)
        return self._books[symbol]

    def process_order(self, order: Order) -> list[Trade]:
        """
        Main entry point. Handles limit and market orders.
        Returns list of trades generated.
        """
        book = self._get_book(order.symbol)
        trades = []

        if order.order_type == OrderType.MARKET:
            # Market orders: match immediately, no resting in book
            trades = self._match_market(book, order)
        else:
            # Limit orders: try to match first, rest remainder in book
            trades = self._match_limit(book, order)

        return trades

    def _match_limit(self, book: OrderBook, incoming: Order) -> list[Trade]:
        """
        Limit order matching:
        - BUY limit: match against asks where ask_price <= buy_price
        - SELL limit: match against bids where bid_price >= sell_price
        """
        trades = []

        if incoming.side == Side.BID:
            # Incoming buy: sweep asks from best (lowest) upward
            while incoming.remaining_qty > 0:
                best_ask = book.best_ask()
                # Stop if no asks, or best ask is above our limit price
                if best_ask is None or best_ask > incoming.price:
                    break
                trade = self._execute_match(book, incoming, book, is_incoming_bid=True)
                if trade:
                    trades.append(trade)
        else:
            # Incoming sell: sweep bids from best (highest) downward
            while incoming.remaining_qty > 0:
                best_bid = book.best_bid()
                # Stop if no bids, or best bid is below our limit price
                if best_bid is None or best_bid < incoming.price:
                    break
                trade = self._execute_match(book, incoming, book, is_incoming_bid=False)
                if trade:
                    trades.append(trade)

        # Rest unfilled portion in the book
        if incoming.remaining_qty > 0:
            # Create a new order for remaining quantity
            resting = Order(
                order_id=incoming.order_id,
                symbol=incoming.symbol,
                side=incoming.side,
                order_type=incoming.order_type,
                price=incoming.price,
                quantity=incoming.remaining_qty,  # Only rest what's left
                timestamp=incoming.timestamp,
            )
            book.add_limit_order(resting)

        return trades

    def _match_market(self, book: OrderBook, incoming: Order) -> list[Trade]:
        """Market orders match against all available liquidity at any price."""
        trades = []
        while incoming.remaining_qty > 0:
            if incoming.side == Side.BID:
                if book.best_ask() is None:
                    break  # No liquidity โ€” partial fill
                trade = self._execute_match(book, incoming, book, is_incoming_bid=True)
            else:
                if book.best_bid() is None:
                    break
                trade = self._execute_match(book, incoming, book, is_incoming_bid=False)
            if trade:
                trades.append(trade)
        return trades

    def _execute_match(
        self,
        book: OrderBook,
        incoming: Order,
        _book: OrderBook,
        is_incoming_bid: bool,
    ) -> Optional[Trade]:
        """
        Execute a single match between incoming order and best resting order.
        Price determination: resting order's price (passive price priority).
        """
        if is_incoming_bid:
            resting = book.pop_best_ask()
        else:
            resting = book.pop_best_bid()

        if resting is None:
            return None

        # Trade quantity = minimum of both remaining quantities
        fill_qty = min(incoming.remaining_qty, resting.remaining_qty)
        # Trade price = resting order's price (maker price wins)
        fill_price = resting.price

        # Update filled quantities
        incoming.filled_qty += fill_qty
        resting.filled_qty += fill_qty

        # If resting order is fully filled, remove from level deque
        if resting.remaining_qty == 0:
            if is_incoming_bid:
                book._ask_levels[resting.price].popleft()
            else:
                book._bid_levels[resting.price].popleft()

        trade = Trade(
            trade_id=str(uuid.uuid4()),
            symbol=incoming.symbol,
            price=fill_price,
            quantity=fill_qty,
            buyer_order_id=incoming.order_id if is_incoming_bid else resting.order_id,
            seller_order_id=resting.order_id if is_incoming_bid else incoming.order_id,
        )
        return trade

    def cancel_order(self, symbol: str, order_id: str) -> bool:
        book = self._get_book(symbol)
        return book.cancel_order(order_id)


# โ”€โ”€โ”€ Example Usage โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
if __name__ == "__main__":
    engine = MatchingEngine()

    # Resting sell orders in the book
    ask1 = Order("A1", "AAPL", Side.ASK, OrderType.LIMIT, price=102.00, quantity=100)
    ask2 = Order("A2", "AAPL", Side.ASK, OrderType.LIMIT, price=101.50, quantity=50)
    ask3 = Order("A3", "AAPL", Side.ASK, OrderType.LIMIT, price=101.50, quantity=75)

    engine.process_order(ask1)  # No trades (no bids yet)
    engine.process_order(ask2)
    engine.process_order(ask3)

    # Incoming buy limit order at 102.00 โ€” should sweep asks at 101.50 first
    buy = Order("B1", "AAPL", Side.BID, OrderType.LIMIT, price=102.00, quantity=200)
    trades = engine.process_order(buy)

    for t in trades:
        print(f"TRADE: {t.quantity} @ {t.price} | buyer={t.buyer_order_id} seller={t.seller_order_id}")
    # Expected:
    # TRADE: 50 @ 101.50 | buyer=B1 seller=A2   (FIFO: A2 first at 101.50)
    # TRADE: 75 @ 101.50 | buyer=B1 seller=A3
    # TRADE: 75 @ 102.00 | buyer=B1 seller=A1   (75 remaining of B1 matches A1)
    # Remaining 0 qty from B1 โ€” fully filled!

็ณป็ปŸๆžถๆž„ๅ›พ / System Architecture

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ Exchange Platform โ”‚ โ”‚ โ”‚ [Broker APIs] โ”€โ”€โ”€โ”€โ”€โ”€โ–บ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ [Web Portal] โ”€โ”€โ”€โ”€โ”€โ”€โ–บ โ”‚ โ”‚ Order โ”‚ โ”‚ Risk Engine โ”‚ โ”‚ [FIX Protocol]โ”€โ”€โ”€โ”€โ”€โ”€โ–บ โ”‚ โ”‚ Gateway โ”‚โ”€โ”€โ”€โ–บโ”‚ (pre-trade โ”‚ โ”‚ โ”‚ โ”‚ (TCP/TLS) โ”‚ โ”‚ checks) โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ–ผ โ–ผ โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ Sequencer / Order Bus โ”‚ โ”‚ โ”‚ โ”‚ (assigns monotonic sequence IDs) โ”‚ โ”‚ โ”‚ โ”‚ Kafka / LMAX Disruptor โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ–ผ โ–ผ โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ Matching โ”‚ โ”‚ Matching โ”‚ โ”‚ โ”‚ โ”‚ Engine โ”‚ โ”‚ Engine โ”‚ โ”‚ โ”‚ โ”‚ (AAPL,GOOG)โ”‚ โ”‚ (TSLA,AMZN) โ”‚ โ”‚ โ”‚ โ”‚ [in-memory]โ”‚ โ”‚ [in-memory] โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ–ผ โ–ผ โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ Trade Reporter โ”‚ โ”‚ โ”‚ โ”‚ (publishes fills to market data) โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ–ผ โ–ผ โ–ผ [Event Log] [Market Data Feed] [Clearing House] (Kafka/WAL) (Level 1/2 quotes) (settlement)

่ฏทๆฑ‚ๆตๅ‘่ฟฝ่ธช / Request Flow Trace (Cross-Check)

Let's trace buy = Order("B1", "AAPL", BID, LIMIT, price=102.00, qty=200) through the diagram and code:

  1. Order Gateway receives order over FIX/TCP, validates format
  2. Risk Engine checks: does client have enough buying power? Position limits?
  3. Sequencer assigns sequence_id=5042 and writes to Kafka topic orders-AAPL
  4. Matching Engine (AAPL) consumes from Kafka, calls engine.process_order(buy)
  5. Code: _match_limit() โ†’ checks best_ask() = 101.50 โ‰ค 102.00 โ†’ calls _execute_match()
  6. Fills 50 @ 101.50 (A2), 75 @ 101.50 (A3), 75 @ 102.00 (A1)
  7. Trade objects emitted โ†’ Trade Reporter publishes to market data feed
  8. Event Log persists each trade with sequence_id for crash recovery

โœ… Diagram boxes match code path exactly.


Part 3: Edge Cases & Gotchas / ่พน็•Œๆƒ…ๅ†ต (2 min)

ไธญๆ–‡๏ผš

ๅœบๆ™ฏ้—ฎ้ข˜่งฃๅ†ณๆ–นๆกˆ
ๅธ‚ไปทๅ•ๆ— ๆตๅŠจๆ€ง้ƒจๅˆ†ๆˆไบคๆˆ–ๅฎŒๅ…จๅคฑ่ดฅ่ฟ”ๅ›žๆˆไบค้‡๏ผŒๆœชๆˆไบค้‡ๆŠฅๅ‘Š็ป™ๅฎขๆˆท๏ผ›ๆŸไบ›ไบคๆ˜“ๆ‰€็›ดๆŽฅๆ‹’็ป
ๆ’ฎๅˆๅผ•ๆ“Žๅดฉๆบƒๅ†…ๅญ˜ไธญ่ฎขๅ•็ฐฟไธขๅคฑ้€š่ฟ‡ Kafka offset ้‡ๆ”พๅ…จ้‡่ฎขๅ•ๆ—ฅๅฟ—้‡ๅปบ
ไปทๆ ผ็ฒพๅบฆๆตฎ็‚น่ฏฏๅทฎๅฏผ่‡ด 101.4999... โ‰  101.5็”จๆ•ดๆ•ฐ่กจ็คบไปทๆ ผ๏ผš$101.50 โ†’ 10150 (cents)
้‡ๅค่ฎขๅ•็ฝ‘็ปœ้‡่ฏ•ๅฏผ่‡ดๅŒไธ€่ฎขๅ•่ขซๅค„็†ไธคๆฌกๅน‚็ญ‰้”ฎ๏ผšorder_id ๅ”ฏไธ€๏ผ›sequencer ๅŽป้‡
ๅŒไปทๆ ผๅคš่ฎขๅ•ๆ’ฎๅˆ้กบๅบFIFO ่ขซ็ ดๅdeque ไฟ่ฏๆ’ๅ…ฅ้กบๅบ๏ผ›timestamp ไฝœไธบ็ฌฌไบŒๆŽ’ๅบ้”ฎ

English:

# CRITICAL: Never use float for prices in production!
# Wrong โŒ
price = 101.50  # float precision issues

# Right โœ… โ€” store as integer cents or fixed-point
price_cents = 10150  # $101.50 in cents

# Or use Python's Decimal
from decimal import Decimal
price = Decimal("101.50")  # Exact representation

The "ghost order" problem: After partial fills, you must update remaining_qty, not create new orders. Otherwise you double-count in the book.

Clock skew: Two matching engines on different machines may see different timestamps. Use the Sequencer's sequence number as the canonical ordering โ€” not wall clock time.


Part 4: Real-World Application / ๅฎž้™…ๅบ”็”จ (2 min)

ไธญๆ–‡ โ€” ็œŸๅฎž็ณป็ปŸๅฆ‚ไฝ•ๅฎž็Žฐ๏ผš

LMAX Disruptor Pattern๏ผˆๆœ€ๅ…ทๅฝฑๅ“ๅŠ›็š„ๆžถๆž„๏ผ‰

  • ๅ•็บฟ็จ‹ๆ’ฎๅˆๅผ•ๆ“Ž + ๆ— ้”็Žฏๅฝข็ผ“ๅ†ฒๅŒบ๏ผˆRing Buffer๏ผ‰
  • ๆฏ็ง’ๅค„็† 600 ไธ‡่ฎขๅ•๏ผŒๅปถ่ฟŸ < 1ฮผs
  • ๆ ธๅฟƒๆดž่ง๏ผšๅ•็บฟ็จ‹ + ้ซ˜ๆ•ˆ้˜Ÿๅˆ— ๆฏ”ๅคš็บฟ็จ‹ + ้”ๆ›ดๅฟซ

Nasdaq / NYSE ็š„ๅšๆณ•๏ผš

  • ๆฏไธช่‚ก็ฅจไธ€ไธช็‹ฌ็ซ‹็š„ๆ’ฎๅˆๅผ•ๆ“Ž๏ผˆ็‰ฉ็†้š”็ฆป๏ผ‰
  • ็”จ FPGA ็กฌไปถๅŠ ้€Ÿๅ…ณ้”ฎ่ทฏๅพ„๏ผˆ็บณ็ง’็บงๅปถ่ฟŸ๏ผ‰
  • ไบ‹ไปถๆบฏๆบ๏ผˆEvent Sourcing๏ผ‰๏ผš่ฎขๅ•็ฐฟ็Šถๆ€ = ๅ…จ้‡ไบ‹ไปถๅ›žๆ”พ

้ž้‡‘่žๅœบๆ™ฏไธญ็š„ๅบ”็”จ๏ผš

  • Airbnb/Uber ๅฎšไปท๏ผšไพ›้œ€ๆ’ฎๅˆ๏ผŒๅŠจๆ€ไปทๆ ผ่ฐƒๆ•ด
  • ๅนฟๅ‘Š็ซžไปท๏ผˆRTB๏ผ‰๏ผšๅฎžๆ—ถ็ซžไปท๏ผŒ100ms ๅ†…ๅฎŒๆˆ
  • ๆธธๆˆไบคๆ˜“ๆ‰€๏ผšSteam ๅธ‚ๅœบใ€Eve Online ็ปๆตŽ็ณป็ปŸ

English โ€” Where this appears in production:

Exchange | Engine | Latency | Notes -------------|-----------------|------------|--------------------------- LMAX | Disruptor | < 1ฮผs | Single-threaded ring buffer Nasdaq | INET | < 100ฮผs | FPGA for hot path NYSE | Pillar | < 100ฮผs | Co-location for HFT firms Binance | Custom C++ | ~1ms | Crypto, simpler rules Interactive | Custom Java | ~1ms | Retail, less HFT pressure Brokers | | |

Key insight for interviews: the matching algorithm is simple (2 priority queues). The hard parts are persistence, fairness, and latency at scale.


Part 5: Interview Simulation / ้ข่ฏ•ๆจกๆ‹Ÿ (3 min)

ๅ…ธๅž‹้ข่ฏ•่ฟ›็จ‹ / Typical Interview Flow

Q1: ไฝ ไผš็”จไป€ไนˆๆ•ฐๆฎ็ป“ๆž„ๆฅๅฎž็Žฐ่ฎขๅ•็ฐฟ๏ผŸ

A: ๆฏไธชไปทๆ ผๆกฃไฝ็”จ deque๏ผˆFIFO ไฟ่ฏ๏ผ‰๏ผŒไปทๆ ผ็ดขๅผ•็”จๅ †๏ผˆheapq๏ผ‰ๅฟซ้€ŸๆŸฅๆ‰พๆœ€ไผ˜ไปทใ€‚ไนฐๅ•็”จๆœ€ๅคงๅ †๏ผˆๅ–ๅ๏ผ‰๏ผŒๅ–ๅ•็”จๆœ€ๅฐๅ †ใ€‚ๆ—ถ้—ดๅคๆ‚ๅบฆ๏ผšๆ’ๅ…ฅ O(log n)๏ผŒๆ’ฎๅˆ O(k log n)๏ผˆk = ๆˆไบค็ฌ”ๆ•ฐ๏ผ‰ใ€‚

Q2: How do you handle the engine crashing mid-trade?

A: Event sourcing + write-ahead log. Every order gets a monotonic sequence ID from the sequencer before entering the engine. On restart, replay all events with seq_id > last_checkpoint. This gives exactly-once semantics. The matching engine is a pure function: same input sequence โ†’ same output.

Q3: ๅฆ‚ไฝ•ๆ”ฏๆŒๅคšไธชไบคๆ˜“ๆ‰€ไน‹้—ด็š„ๆœ€ไผ˜ไปทๆ ผ่ทฏ็”ฑ๏ผˆSmart Order Routing๏ผ‰๏ผŸ

A: SOR ๅฑ‚ๅœจๆ’ฎๅˆๅผ•ๆ“Žไน‹ไธŠใ€‚ๅฎƒ่ฎข้˜…ๅคšไธชไบคๆ˜“ๆ‰€็š„ Level 2 ๅธ‚ๅœบๆทฑๅบฆๆ•ฐๆฎ๏ผˆ่ฎขๅ•็ฐฟๅฟซ็…ง๏ผ‰๏ผŒ่ฎก็ฎ—ๆœ€ไผ˜ๆ‰ง่กŒ่ทฏๅพ„๏ผŒๅฐ†ๅคง่ฎขๅ•ๆ‹†ๅˆ†๏ผˆOrder Splitting๏ผ‰ๅ‘้€ๅˆฐไธๅŒไบคๆ˜“ๆ‰€ใ€‚ๅ…ณ้”ฎๆŒ‘ๆˆ˜๏ผšๅ„ไบคๆ˜“ๆ‰€ๆ•ฐๆฎๆœ‰ๅปถ่ฟŸ๏ผŒ้œ€่ฆๅค„็†่ฟ‡ๆœŸๆŠฅไปทใ€‚

Q4: What's the difference between a limit order book and an auction?

A: Limit order book = continuous matching (every new order tries to match immediately). Auction = collect orders for a time period, then batch-match at a single clearing price (used at market open/close). Auctions reduce the advantage of being microseconds faster; continuous books reward speed.

Q5: How would you scale this to handle 10 million orders/second?

A: Three strategies:
1. Partition by symbol โ€” each symbol is its own stateful engine on a dedicated thread/process. No cross-symbol state needed.
2. LMAX Disruptor โ€” replace Kafka with a single-producer/multi-consumer lock-free ring buffer for ~10x lower latency.
3. FPGA offload โ€” the inner matching loop (heap operations, price comparison) maps well to hardware; Nasdaq uses this for <100ns latency.
4. No distributed transactions โ€” each symbol's book is authoritative and independent. Cross-symbol atomicity (e.g., pairs trading) happens at a higher layer.

ๆ€ป็ป“ / Summary

ๆ ธๅฟƒ่ฆ็‚น / Key Takeaways:

1. ๆ•ฐๆฎ็ป“ๆž„: heap (ไปทๆ ผๆŽ’ๅบ) + deque (FIFO within level) + hashmap (O(1) cancel)
2. ๆ’ฎๅˆ่ง„ๅˆ™: price-time priority, resting price wins, lazy deletion for cancel
3. ๆŒไน…ๅŒ–: event sourcing โ€” ่ฎขๅ•็ฐฟๆ˜ฏไบ‹ไปถๆ—ฅๅฟ—็š„็‰ฉๅŒ–่ง†ๅ›พ
4. ๆ€ง่ƒฝ: ๅ•็บฟ็จ‹ + ๆ— ้” > ๅคš็บฟ็จ‹ + ้” (LMAX ่ฏๆ˜Ž)
5. ่ธฉๅ‘: ๆฐธ่ฟœ็”จๆ•ดๆ•ฐ/Decimal่กจ็คบไปทๆ ผ๏ผŒไธ่ฆ็”จfloat

Algorithm complexity:
  - Add limit order:  O(log P) where P = distinct price levels
  - Match:           O(k log P) where k = number of fills
  - Cancel:          O(1) lazy deletion

้ข่ฏ•ไธ€ๅฅ่ฏ: "The algorithm is two priority queues. The engineering is making it fast, fair, and crash-safe."

๐Ÿ”— References: