Engineering Atlas › Low-Level Design

Code an order-matching stock exchange

12 min read · last reviewed 2026-10-17

Open in the interactive app: practice, quiz, mock interview →

In one minute

Requirements

Functional:

Non-functional:

Out of scope: margin and short selling, options, settlement with a clearing house, regulatory reporting, and the network and replication layer. Name them, then move on.

Entities and relationships

Start by asking which nouns own state that changes under concurrent requests.

Relationships: an exchange aggregates accounts and books; a book is composed of resting orders. An order references its account by id, because accounts outlive orders and are locked independently. Holds belong to orders and are released when the order reaches a terminal state.

Class diagram

classDiagram
    class Exchange {
        +placeOrder(req) ExecutionReport
        +cancel(orderId) ExecutionReport
        +deposit(accountId, cents)
        -onTrade(trade)
    }
    class Account {
        +id
        -cashTotal
        -cashHeld
        +availableCash() int
        +hold(cents)
        +release(cents)
        +settleBuy(qty, price, limit)
        +settleSell(symbol, qty, price)
    }
    class Position {
        +symbol
        -total
        -held
    }
    class Order {
        +id
        +side
        +type
        +qty
        +remaining
        +limitPrice
        +stopPrice
        +status
        +transition(event)
    }
    class OrderValidator {
        <<interface>>
        +check(order, account) Result
    }
    class OrderBook {
        +symbol
        +add(order) List~Trade~
        +remove(orderId)
    }
    class StopBook {
        +park(order)
        +onTradePrice(symbol, price) List~Order~
    }
    class Trade {
        +buyOrderId
        +sellOrderId
        +price
        +qty
        +seq
    }
    class ExecutionListener {
        <<interface>>
        +onReport(report)
    }
    Exchange o-- Account
    Exchange o-- OrderBook
    Exchange o-- StopBook
    Exchange --> OrderValidator
    Exchange --> ExecutionListener
    Account *-- Position
    OrderBook *-- Order
    OrderBook ..> Trade

Key flows

The flow that interviewers probe is a buy limit order that partially fills, followed by a sell trade that triggers someone else's stop.

sequenceDiagram
    participant U as User
    participant X as Exchange
    participant A as Account
    participant B as OrderBook
    participant S as StopBook
    participant L as Listeners
    U->>X: place BUY 10 ACME LIMIT 500
    X->>X: validate (tick, lot, price band)
    X->>A: hold 10 x 500 = 5000
    A-->>X: ok
    X->>L: NEW
    X->>B: add order
    B-->>X: Trade 4 @ 480
    X->>A: settleBuy 4 @ 480 (release 2000, debit 1920)
    X->>L: PARTIALLY_FILLED 4/10
    X->>S: onTradePrice(ACME, 480)
    S-->>X: stop order S7 triggered
    X->>A: hold for S7 at activation
    X->>B: add S7 as limit
    U->>X: cancel
    X->>B: remove order
    X->>A: release 6 x 500 = 3000
    X->>L: CANCELLED

Two details in that diagram matter. The fill releases the hold at the limit price (4 x 500) but debits at the trade price (4 x 480), so the 80 cents of price improvement returns to the user immediately. And the stop order is processed after the trade that triggered it has been fully applied, never in the middle of it, so the account state it is checked against is consistent.

Design patterns used and why

A pattern that does not belong: a Singleton exchange. Inject one instance per process from main, so tests can create twenty.

Concurrency and edge cases

Down the stack

Layer 1The contract

For every account, always, 0 <= held <= total for cash and each position. Accepting an order either places a hold covering its worst case, or changes nothing and returns REJECTED. A fill of q at price p on a buy order with limit L requires p <= L and q <= remaining, and moves exactly q x L out of held and q x p out of total. Cancelling releases the hold on the remaining quantity. Terminal orders never change again.

Layer 2The mechanism

The account is a pair of integers per asset, and each order can derive its outstanding hold (remaining x limit for buys, remaining shares for sells). Dormant stops sit in two heaps per symbol: buy stops in a min-heap by stop price, sell stops in a max-heap. On each trade the exchange pops every stop the price crosses, then activates them in placement sequence. In FIX, each transition produces an ExecutionReport (MsgType 8) whose OrdStatus (tag 39) is 0 New, 1 Partially filled, 2 Filled, 4 Canceled or 8 Rejected, so the state table and the wire protocol line up.

Layer 3The machine

An account update is two 64-bit integer writes, a few nanoseconds in L1 cache. What costs time is coordination: a contended mutex falls into a futex syscall and costs microseconds, a database row lock with a WAL fsync costs 0.1 to 1 ms on NVMe. So production exchanges keep books and balances in memory on single-threaded loops and journal input commands in batches, one fsync per thousands of orders. A heap of 10,000 stops is 14 levels deep, so finding triggered stops is cheap; a cascade costs its trades.

Why it's built this way

  1. A negative balance is a loss someone absorbs, so every order reserves its worst case before it trades.
  2. The reservation must be atomic with the check, so the account is the unit of serialization.
  3. Hot accounts (market makers) receive thousands of fills per second, so contention sets throughput, which pushes toward single-writer loops.
  4. Customers and regulators ask why things happened, so every change is a logged event and state is a fold over the log.

Extensibility

Leadership lens

Check yourself

  1. Why does a buy fill release qty x limit from held but debit only qty x trade price from total?

    Show answer

    The hold reserved the worst case at the limit price. When the trade happens at a better price, the difference is price improvement that belongs to the customer, so the full reserved amount for those shares is released and only the actual cost is spent.

  2. Why should a cascade of triggered stop orders be processed through a queue rather than recursively from the matching loop?

    Show answer

    Recursion starts a new order while the previous trade is half applied and orders priority by call depth. A queue applies each trade fully, then activates stops in a defined order.

  3. A user cancels an order that has filled 4 of 10 shares. How much of the hold is released?

    Show answer

    Only the hold for the remaining 6 shares. The 4 filled shares already released their hold at fill time, so releasing on the original quantity would double count and push held below zero.

  4. Why prefer an enum plus small strategies over MarketOrder, LimitOrder and StopOrder subclasses?

    Show answer

    A stop order turns into a market or limit order when it triggers, which a fixed class cannot do without copying. Keeping type as data lets the order change behaviour in place while keeping one identity and one history.

Watch

Free resources

Open in the interactive app: practice, quiz, mock interview →