Limit Order Book Matching Engine
Reported by candidates from Citadel's online assessment. Pattern, common pitfall, and the honest play if you blank under the timer.
The edge case that kills a naive limit order book is partial fills across price levels, and Citadel's July 2026 OA leans on it hard. You get up to 200000 operations, so rescanning every resting order per submission won't survive. The pattern is sorted price levels with a FIFO queue at each level, which is a heap or ordered map plus queues. If you've got an OA invite and 48 hours, this is the one to understand cold. StealthCoder is the safety net if you blank mid-assessment, but the structure below is simple enough to hold in your head.
The problem
Process a finite sequence of limit-order submissions for one asset. Each operation has one of these forms: ["BUY", orderId, price, quantity] ["SELL", orderId, price, quantity] All order IDs are unique. Process operations in input order. An incoming buy may trade with resting sells whose price is at most its limit, while an incoming sell may trade with resting buys whose price is at least its limit. Always match the best available price first: the lowest sell price for a buy, or the highest buy price for a sell. Within one price level, match resting orders in arrival order. Each trade uses the resting order's price and the largest quantity possible between the incoming and resting orders. A partially filled incoming order continues matching; any remainder rests at the back of its price level. Return every trade in execution order as "buyOrderId|sellOrderId|price|quantity". Submissions that do not trade produce no output. Function matchOrders(operations: String[][]) → String[] Examples Example 1 operations = [["BUY","b1","100","5"],["SELL","s1","90","2"],["SELL","s2","100","4"],["BUY","b2","101","1"]] return = ["b1|s1|100|2","b1|s2|100|3","b2|s2|100|1"] Sell s1 first consumes two units of resting buy b1 at b1's price. Sell s2 consumes the remaining three units, then its one-unit remainder rests and is filled by b2 at s2's resting price. Example 2 operations = [["SELL","s1","105","2"],["SELL","s2","103","1"],["BUY","b1","106","3"]] return = ["b1|s2|103|1","b1|s1|105|2"] The incoming buy crosses both sells, but the lower price 103 has priority over 105. Example 3 operations = [["BUY","b1","100","2"],["BUY","b2","100","3"],["SELL","s1","101","4"],["SELL","s2","100","4"]] return = ["b1|s2|100|2","b2|s2|100|2"] Sell s1 does not cross the buy price. Sell s2 does, and equal-price buys fill in first-in-first-out order. Constraints 1 <= operations.length <= 200000 Every operation has exactly four strings and one of the forms described above. orderId contains only ASCII letters, digits, hyphens, and underscores, has length from 1 through 40, and does not contain |. Every order ID is unique. 1 <= price, quantity <= 10^9, written as base-10 integers. The number of generated trades and every remaining quantity fit in memory and signed 64-bit arithmetic.
Reported by candidates. Source: FastPrep
Pattern and pitfall
Keep two books. Buys go in a max-heap by price, sells in a min-heap by price. Each price maps to a deque of resting orders in arrival order. For an incoming buy, loop while the lowest sell price is at most the limit and quantity remains. Trade min(incoming, resting) at the resting order's price, not the incoming one. That's the first pitfall. Output order is buyId|sellId, regardless of which side was incoming. That's the second. Pop a resting order when it hits zero, pop the level when its deque empties, and only rest the remainder after matching ends. With heaps, don't push the same price twice, or track levels in a map. Use 64-bit math and parse strings to ints once. If the heap logic or FIFO bookkeeping slips live, StealthCoder can feed you a working structure in real time.
The honest play: practice the pattern, and have StealthCoder ready for the one you didn't see coming.
You can drill Limit Order Book Matching Engine cold, or you can hedge it. StealthCoder runs invisibly during screen share and surfaces a working solution in under 2 seconds. The proctor sees the IDE. They don't see what's behind it. Built for the candidate who saw this exact problem leak two days before his OA and wondered if anyone had a play.
Get StealthCoderRelated leaked OAs
You've seen the question.
Make sure you actually pass Citadel's OA.
Citadel reuses patterns across OAs. Built for the candidate who saw this exact problem leak two days before his OA and wondered if anyone had a play. Works on HackerRank, CodeSignal, CoderPad, and Karat.
Limit Order Book Matching Engine FAQ
What's the trick in the Citadel limit order book problem?+
Sorted price levels with FIFO queues per level. Use a min-heap for sells and a max-heap for buys, with a deque of orders per price. Match best price first, then arrival order, and trade at the resting order's price every time.
Which price does a trade execute at?+
The resting order's price, never the incoming order's. In Example 1, sell s1 at 90 hits resting buy b1 at 100 and trades at 100. Candidates who use the incoming price fail the first sample immediately.
How hard is this really with 200000 operations?+
Medium-hard on implementation, not on ideas. The logic is a simple matching loop. The difficulty is bookkeeping: partial fills, popping empty levels, and resting remainders. Anything that scans all orders per submission is too slow at this size.
What output order should trades use?+
Execution order, with each string formatted buyOrderId|sellOrderId|price|quantity. The buy ID always comes first even when the incoming order is a sell. Mixing that up is a common reason a correct matching loop still fails tests.
How do I prepare for this in 48 hours?+
Write the matcher once from scratch. Practice heap plus map of deques, then test the three examples by hand. Add cases for a remainder that rests, multi-level sweeps, and equal-price FIFO. Reusing the matching loop for both sides cuts bugs.