Online Stock Exchange
A tiny exchange: traders place buy and sell orders on an instrument, and something has to
decide which ones turn into a trade. The class model matters more than the matching
algorithm here - get Order and OrderBook right and the matching logic is a dozen lines.
Requirements
Functional
- A trader places a buy or sell order for a quantity of an instrument, either at a fixed price (limit order) or at whatever the best available price is (market order).
- A buy order matches a sell order when their prices cross; a trade is created for the overlapping quantity.
- Partially filled orders stay on the book for their remaining quantity.
- A trader can cancel an order that hasn't fully filled yet.
Non-functional
- Matching must always prefer the best price first (highest bid, lowest ask), then earliest order at that price - price-time priority, not first-come-first-served across all prices.
- Adding a new order type (say, stop-loss) should extend
Order, not require rewritingOrderBook's matching loop.
Design
OrderBook holds two sides - bids and asks - each kept sorted by price-time priority, and
its match method only ever looks at the best order on each side. Order itself carries
whether it's a limit or market order as data (a price, or none), so the book doesn't need a
different code path per order type.
- 1The trader submits an order for one instrument - the exchange routes by instrument symbol.
- 2The order lands on the correct side (bids or asks) of that instrument’s book, sorted by price-time priority.
- 3The book peeks at the best bid and best ask; if they cross, a fill happens for the overlapping quantity.
- 4A trade is recorded once a match is found; both orders’ remaining quantities are reduced.
- 5A trader can pull an order that hasn’t fully filled; the book removes it from its side.
A Trade is the one output of matching that outlives the match call - it's what both
traders and any downstream settlement system actually care about, independent of how the
book got there.
Class diagram
Code
Design decisions
OrderBookis per-instrument, never a single book for the whole exchange. Orders for AAPL and orders for GOOG never interact, so keeping one book per instrument means matching is naturally scoped and one busy instrument's order volume never slows down matching for a quiet one.- A market order is a limit order with no price, not a separate class. Both are the same object with the same fields; a market order simply matches against the best available price on the other side instead of requiring its own price to cross. Splitting it into its own class would duplicate every field except one.
- Price-time priority is an ordering rule on the book's data structure, not a loop
condition scattered through
match. Bids are kept sorted highest-price-first (ties broken by earlier timestamp), asks lowest-price-first, somatchonly ever has to peek at the head of each side rather than scan for "the best one." - What's missing for a real system: this book is single-threaded and in-memory; a real exchange needs the match step to be atomic under concurrent order submission (a lock per instrument, or a single-writer queue per book), and needs every fill to be durably logged before being acknowledged, since a trade can't be un-happened once reported to a trader.
Common follow-ups
- What happens to a market order that arrives when the opposite side of the book is
empty?
match()'s while loop simply never runs an iteration, so the market order sits on the book indefinitely as if it were a very aggressive limit order. A real exchange would reject or expire an unfilled market order immediately instead of resting it, since "take the best available price" is meaningless with no counter-liquidity. - How would you add a stop-loss order? A
StopLossOrdercarrying a trigger price;OrderBook.matchwouldn't touch it directly - a separate watcher monitors trade prices and, once triggered, hands the order to the book as an ordinary market or limit order, leavingmatch()'s core loop untouched. - Two orders at the same price, submitted a millisecond apart - which fills first, and
where is that guaranteed? The earlier one, guaranteed by
addOrder's sort comparator breaking price ties onplacedAt- price-time priority isn't a runtime check insidematch(), it's baked into how the book stays sorted the moment an order is inserted. - Why would one
OrderBookfor the whole exchange be a problem at scale? Everymatch()call would need to filter for the right instrument, and a burst of volume in one hot stock would contend for the same book and slow matching for every other, unrelated instrument - one book per instrument means AAPL's volume never touches GOOG's book.
Check yourself
Why is a market order modeled as an Order with price set to none, instead of a separate MarketOrder class?