Order System Development (Limit, Market, Stop)
The order system is the heart of any exchange. This is where all trading logic is concentrated, and this is where the cost of error is maximum: a bug in matching engine can liquidate liquidity or create unfair executions in seconds. Let's explore architecture from data model to matching algorithm.
Order Types and Their Semantics
Limit Order
User specifies price and volume. Order executes only if market reaches specified price or better.
- Buy limit: executes at price ≤ specified
- Sell limit: executes at price ≥ specified
- Can be partially filled
- Unfilled part remains in order book
Additional modifiers: GTC (Good Till Cancelled), GTD (Good Till Date), IOC (Immediate Or Cancel), FOK (Fill Or Kill), Post-Only.
Market Order
Executes immediately at best available price. Guarantees execution, not price. On illiquid markets can have significant slippage.
Safe implementation: slippage limit. If execution requires traversing the book more than X%, order is rejected with PRICE_IMPACT_TOO_HIGH.
Stop Order
Trigger order. Activates when market price reaches stop price. After activation becomes market or limit order.
- Stop-Market: market order is created when stop price reached
- Stop-Limit: limit order is created with specified limit price
- Trailing Stop: stop price follows market at specified distance
Matching Engine Architecture
Order Book Data Structure
Classic implementation — two sorted maps (bid side and ask side) with price as key. At each price level — queue of orders (FIFO for price-time priority).
type PriceLevel struct {
Price decimal.Decimal
Orders []*Order // FIFO queue
Total decimal.Decimal // cached volume
}
type OrderBook struct {
Bids *btree.BTree // descending (max bid first)
Asks *btree.BTree // ascending (min ask first)
mu sync.RWMutex
}
Data structure choice is critical for performance:
-
Red-Black Tree (Go
btree, JavaTreeMap): O(log n) insert/delete - Skip List: concurrent without global lock
- Array + binary search: faster for reads on small books
For exchange with <10,000 active orders in book — btree is more than enough.
Matching Algorithm
Price-time priority (FIFO matching) — standard for most exchanges.
Data Model
CREATE TABLE orders (
id UUID PRIMARY KEY,
user_id BIGINT NOT NULL,
pair_id SMALLINT NOT NULL,
side SMALLINT NOT NULL, -- 0=buy, 1=sell
type SMALLINT NOT NULL, -- 0=limit, 1=market, 2=stop_limit, 3=stop_market
status SMALLINT NOT NULL DEFAULT 0, -- 0=open, 1=partial, 2=filled, 3=cancelled
price NUMERIC(36,18),
stop_price NUMERIC(36,18),
quantity NUMERIC(36,18) NOT NULL,
filled_qty NUMERIC(36,18) NOT NULL DEFAULT 0,
time_in_force SMALLINT NOT NULL DEFAULT 0,
expire_at TIMESTAMPTZ,
client_order_id VARCHAR(64),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE trades (
id BIGSERIAL PRIMARY KEY,
pair_id SMALLINT NOT NULL,
taker_order_id UUID NOT NULL,
maker_order_id UUID NOT NULL,
taker_user_id BIGINT NOT NULL,
maker_user_id BIGINT NOT NULL,
price NUMERIC(36,18) NOT NULL,
quantity NUMERIC(36,18) NOT NULL,
taker_fee NUMERIC(36,18) NOT NULL,
maker_fee NUMERIC(36,18) NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
Critical moment: matching engine works in memory, database used only for persistence. On startup server loads all open orders into memory. Database writes are asynchronous through queue.
Stop Orders and Trigger Mechanism
Stop orders stored in separate structure — sorted by stop price. On each trade, matching engine publishes last price. Stop orders processor subscribes to price updates.
Trailing stop — special case. When price moves in user's favor, stop price recalculates.
Balance Control and Atomicity
Before placing order need to reserve funds:
- Buy limit: reserve
price * quantityin quote currency - Sell limit: reserve
quantityin base currency - Market buy: reserve
maxSpend
On cancellation — release reserve. On fill — move reserve to real counterparty balance.
Atomicity through database transactions, but matching engine can't hit database on each match — bottleneck. Solution: in-memory balance with asynchronous synchronization to database.
Decimal Precision and Floating Point
Never use float64 for financial calculations. For finance use:
- Go:
shopspring/decimal - Python:
decimal.Decimal - Java:
BigDecimal - JavaScript:
decimal.jsorbignumber.js
All stored values in database — NUMERIC(36,18) or strings.
Performance and Scaling
One Go matching engine handles 50,000–100,000 orders/sec on modern hardware at latency <1ms. For most CEX this is sufficient.
When scaling needed — sharding by trading pair. BTC/USDT — separate instance, ETH/USDT — separate. Pairs don't interact at matching level.
| Component | Technology | Latency |
|---|---|---|
| In-memory order book | Go btree | <100µs |
| Stop orders processor | Go goroutine | <1ms |
| Balance check | In-memory map | <10µs |
| DB persistence | PostgreSQL async | 5–20ms |
| WebSocket broadcast | Go channels | <1ms |
Testing
Matching engine covered with unit tests on edge cases:
- Partial fill with remainder
- FOK with insufficient liquidity
- IOC with partial execution
- Simultaneous cancellation and fill
- Stop order triggering at placement moment
- Decimal overflow at extreme values
Property-based testing (fuzzing) — random order sequences generated, invariant checked: total buy volume = total sell volume, balances reconcile.
Development Timeline
- MVP (limit + market, no stop, no time-in-force): 3–4 weeks
- Full system with stop orders, all TIF modifiers, trailing stop: 8–12 weeks
- Production-ready with audit, load tests, monitoring: +4–6 weeks
Cost depends on throughput requirements and availability of ready components (balance, fees, WebSocket).







