Code an order-matching stock exchange
Open in the interactive app: practice, quiz, mock interview →In one minute
- Seen as an object model, an online exchange is an account ledger (cash and shares, each split into total and held), an order book, and an order lifecycle. Matching is the famous part; holds and the lifecycle are where most real bugs live.
- Reserve before you route. A buy order locks
quantity x limit priceof cash and a sell order locks the shares when it is accepted. Without holds, two orders spend the same dollar. - Order types differ in when they become live and what price they accept. Model the differences as data plus a small set of behaviours (validation, trigger condition, price acceptance), not as a deep class hierarchy.
- Stop orders are dormant orders with a trigger. They wake on a trade, in a defined order, and must be re-checked against the account, because the money may be gone.
- This lesson is the in-process object model. For sequencers, replicated journals, multicast market data and microsecond latency, see Design Stock Exchange.
Requirements
Functional:
- Users have accounts with a cash balance and share positions per symbol. They can deposit cash and see what is available to trade.
- Users place market, limit, stop and stop-limit orders to buy or sell a symbol, and can cancel an order that is not yet complete.
- The exchange matches buy and sell orders per symbol, produces trades, and updates both accounts.
- Users receive execution reports (accepted, partially filled, filled, cancelled, rejected) with reasons.
Non-functional:
- No account can ever spend cash or shares it does not have, including when several orders race.
- Every state change is explainable: an order's current state is the result of an ordered list of events.
- Money and prices are integers (cents, or ticks), never floating point.
- Deterministic behaviour: the same sequence of inputs gives the same trades and the same balances.
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.
- Account owns cash in integer cents as two numbers:
totalandheld.available = total - held. It also owns one Position per symbol with the same split for shares. - Order is the unit of intent: id, account id, symbol, side, type, quantity, remaining, optional limit and stop prices, time in force and status. It changes only through lifecycle transitions.
- OrderType is an enum (MARKET, LIMIT, STOP, STOP_LIMIT). These map one-to-one onto FIX tag 40, which is how real brokers send them.
- Hold is the reservation an accepted order places on an account. In code it is often just the
heldcounters plus the order's remaining quantity, but naming it keeps the invariant visible. - OrderBook holds one symbol's resting limit orders. Its internals belong to the system design lesson; here it accepts an order and emits trades.
- Trade is an immutable record: buy order id, sell order id, symbol, price, quantity, sequence number.
- StopBook holds dormant stop orders per symbol, waiting for a trade price to cross their stop price.
- Exchange is the façade. It validates, places holds, routes to the book, applies trades to accounts and publishes execution reports.
- ExecutionListener receives reports: the user's websocket, the audit log, market data.
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 ..> TradeKey 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: CANCELLEDTwo 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
- State for the order lifecycle. The legal transitions are few and strict: PENDING (dormant stop) to NEW or REJECTED; NEW to PARTIALLY_FILLED, FILLED or CANCELLED; PARTIALLY_FILLED to FILLED or CANCELLED. Terminal states accept nothing. A transition table keyed by (status, event) is easier to audit than scattered
ifchains, and makes "fill on a cancelled order" an explicit error. The State lesson shows the class-per-state version; a table is enough here. - Chain of Responsibility for pre-trade validation. Checks are independent and grow over time: positive quantity, tick grid, lot size, price band, symbol halted, account restricted, available funds. Each passes or returns a reason code; new regulation adds a link. See Chain of Responsibility.
- Strategy for order-type behaviour. A market order accepts any price; a limit order accepts prices at or better than its limit; a stop order has a trigger predicate (buy stops fire when the trade price rises to the stop, sell stops when it falls to it). Encoding "accepts price" and "is triggered by" as small functions per type keeps the matching loop free of type checks.
- Observer for execution reports. The user channel, audit log and risk monitor all need every report; the exchange should not know they exist.
- Command for inbound requests.
PlaceOrder,CancelOrderandDepositare serialisable commands with a sequence number. Logging commands rather than state lets you rebuild the exchange by replay, the idea behind event sourcing and LMAX. - Façade:
Exchangeis the single entry point that enforces the order validate, hold, route, apply, notify.
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
- Two orders, one balance. A user with 5,000 cents sends two 4,000 buys from two tabs. If the check and the hold are separate steps, both pass. Make them one step under the account's lock, or one conditional update (
UPDATE account SET held = held + $1 WHERE id = $2 AND total - held >= $1, checking the row count). - Lock ordering. A trade touches two accounts. Lock them in a fixed order (by account id), or better, apply account changes on an account-owning thread so no path holds two locks.
- Stop cascades. A trade at 95 triggers sell stops, which trade lower and trigger more. Queue triggered stops in placement order, each after the previous trade is fully applied. Recursing from the matching loop gives priority by stack depth.
- Stop activation without funds. A stop placed on Monday activates on Thursday, after the cash was spent. Either hold at placement (safe, ties up money) or check at activation and reject with a reason (flexible, but the rejection arrives at the worst moment). Pick one and show it on the confirmation screen.
- Market orders and holds. A market buy has no limit, so the hold is unknown. Convert it to a limit with a protection band (say 5 percent above the last trade) or hold the full balance; never accept an unbounded buy.
- Cancel races fill. Whichever the book processes first wins. The cancel releases the hold on the quantity remaining at that moment, not the original quantity.
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
- A negative balance is a loss someone absorbs, so every order reserves its worst case before it trades.
- The reservation must be atomic with the check, so the account is the unit of serialization.
- Hot accounts (market makers) receive thousands of fills per second, so contention sets throughput, which pushes toward single-writer loops.
- Customers and regulators ask why things happened, so every change is a logged event and state is a fold over the log.
Extensibility
- Trailing stop: a new strategy whose stop price follows the trade price, using the trade hook the stop book already has.
- Time in force (IOC, FOK, GTC, day): a field plus a rule after matching: IOC cancels the remainder, FOK needs a full fill, day orders are cancelled by a command at the close, driven by an injected clock.
- Fees: a fee strategy at settlement; holds must include the maximum fee.
- Margin accounts: buying power becomes
cash + borrowing limit; theavailablecalculation moves behind an interface. - Many machines: books partition by symbol, accounts by account id; cross-partition trades need the patterns in Design Stock Exchange and Design Digital Wallet.
Leadership lens
- Build first: the ledger with holds and a randomized test asserting
0 <= held <= totalafter every place, fill and cancel. A ledger that leaks money cannot ship. - Ask in review: where is each hold released, for every terminal state; what happens to a stop when funds are gone; how are simultaneous triggers ordered.
- Treat the order status table as a product document: support, compliance and mobile all read it.
- Split ownership along the class diagram's seams (accounts and risk, matching, reports), with command and event formats as the contract.
- In an interview, spend the time on holds, the lifecycle and stops, not an
Orderhierarchy.
Check yourself
Why does a buy fill release
qty x limitfrom held but debit onlyqty x trade pricefrom 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.
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.
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.
Why prefer an enum plus small strategies over
MarketOrder,LimitOrderandStopOrdersubclasses?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
- How to Build an Exchange (Jane Street): how real exchanges structure order handling as deterministic state machines fed by a sequenced log.
- State Pattern, Design Patterns ep 17 (Christopher Okhravi): the pattern behind the order lifecycle, explained slowly and carefully.
- Observer Pattern, Design Patterns ep 2 (Christopher Okhravi): how execution reports fan out to listeners without coupling.
Free resources
- Order (exchange) (Wikipedia)): definitions of market, limit, stop and stop-limit orders and common time-in-force options.
- FIX 4.4 OrdStatus, tag 39 (OnixS dictionary): the industry's own list of order states, a good check on your state table.
- FIX 4.4 OrdType, tag 40 (OnixS dictionary): the order types brokers actually send.
- The LMAX Architecture (Martin Fowler): an in-memory, single-threaded business logic processor for a retail exchange, with replay for recovery.
- Chain of Responsibility (Refactoring.Guru): the structure behind pluggable pre-trade checks.