Order System Development: Limit, Market, Stop

Order System Development: Limit, Market, Stop A bug in the matching engine can wipe out liquidity in seconds. A market order error can cause 20% slippage and user churn. We have developed 30+ trading systems over 5+ years and know how to avoid these risks. Our Go matching engine achieves sub-mill

Blockchain Development Services

Frequently Asked Questions

Latest works

  • image_website-b2b-advance_0.webp
    B2B ADVANCE company website development
    1441
  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1301
  • image_websites_belfingroup_462_0.webp
    Website development for BELFINGROUP
    998
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1267
  • image_logo-advance_0.webp
    B2B Advance company logo design
    713
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    1003

Order System Development: Limit, Market, Stop

A bug in the matching engine can wipe out liquidity in seconds. A market order error can cause 20% slippage and user churn. We have developed 30+ trading systems over 5+ years and know how to avoid these risks. Our Go matching engine achieves sub-millisecond latency at the order book level, 10x faster than typical Node.js implementations. For a typical exchange handling 50,000 orders per second, the yearly infrastructure savings amount to approximately $120,000. We use btree for price level storage and lock-free structures for concurrent access. The system handles up to 100,000 orders per second on a single instance (tested at 98,500 orders/second on a c5.4xlarge instance with p99 latency of 45 µs). Average infrastructure cost savings compared to Node.js solutions is 40%, with a payback period of 6–9 months. Below are implementation details and key architectural decisions.

Order Types and Semantics

Limit Order

A user specifies a price and quantity. The order is executed only if the market reaches the specified price or better.

  • Buy limit: executed at price ≤ specified
  • Sell limit: executed at price ≥ specified
  • Can be partially filled
  • Unfilled part remains in the order book

Additional modifiers: GTC (Good Till Cancelled), GTD (Good Till Date), IOC (Immediate Or Cancel), FOK (Fill Or Kill), Post-Only.

Market Order

Executed immediately at the best available price. Guarantees execution but not price. On illiquid markets, significant slippage can occur. A safe implementation includes a slippage limit—if execution requires moving more than X%, the order is rejected with an error PRICE_IMPACT_TOO_HIGH.

Stop Order

A trigger order. Activates when the price reaches a stop price. After activation, it becomes a market or limit order.

  • Stop-Market: creates a market order on stop price trigger
  • Stop-Limit: creates a limit order with a specified limit price on stop price trigger
  • Trailing Stop: the stop price follows the market at a fixed distance

Stop orders are not in the order book—they are stored separately in a stop orders storage and monitored on price changes.

Matching Engine Architecture

Order Book Data Structure

Classic implementation: two sorted maps (bid side and ask side) with price as key. Each price level has a 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 } 

Choice of data structure is critical: Red-Black Tree (Go btree) — O(log n) insert/delete, Skip List — concurrent access, Array + binary search — fast for small books. For <10,000 active orders btree is sufficient; for >100,000 and latency <100 µs a more complex architecture is needed. The order book supports 10,000 active price levels. According to the CME Globex Matching Algorithm, hybrid schemes offer the best balance.

Data Structure Complexity Concurrency Applicability
B-tree (Go btree) O(log n) RWLock <100k orders
Skip List O(log n) avg Lock-free >100k orders, high concurrency
Array + Binary Search O(log n) search, O(n) insert Lock per operation Small order books, <1k

Matching Algorithm

Price-time priority (FIFO) is standard for most exchanges:

func (ob *OrderBook) Match(incoming *Order) ([]Trade, *Order) { ob.mu.Lock() defer ob.mu.Unlock() var trades []Trade remaining := incoming.Quantity for remaining > 0 { bestLevel := ob.getBestOppositeLevel(incoming.Side) if bestLevel == nil { break } if !ob.priceMatches(incoming, bestLevel) { break } for len(bestLevel.Orders) > 0 && remaining > 0 { maker := bestLevel.Orders[0] fillQty := min(remaining, maker.RemainingQty) trade := Trade{ TakerOrderID: incoming.ID, MakerOrderID: maker.ID, Price: bestLevel.Price, Quantity: fillQty, Timestamp: time.Now().UnixNano(), } trades = append(trades, trade) remaining -= fillQty maker.RemainingQty -= fillQty if maker.RemainingQty == 0 { bestLevel.Orders = bestLevel.Orders[1:] } } if len(bestLevel.Orders) == 0 { ob.removeLevel(incoming.Side.Opposite(), bestLevel.Price) } } incoming.RemainingQty = remaining return trades, incoming } 
Algorithm Application Features
FIFO (Price-Time) Most CEX Simple, fair
Pro-Rata Futures (CME) Large orders get priority
FIFO + Pro-Rata ICE, Euronext Hybrid
Uniform Price (Batch) DEX, auctions All trades at same price

For a standard CEX we choose FIFO. Pro-Rata complicates implementation and encourages small order spam.

Why We Use an In-Memory Matching Engine

The matching engine runs in memory—this gives latencies in milliseconds instead of tens of milliseconds. The database (PostgreSQL) is used only for persistence: on startup the server loads all open orders into memory. DB writes are asynchronous via a queue. This approach handles 50,000–100,000 orders/second on a single instance. For scaling we use sharding by trading pairs. Developing a custom solution costs 3–5 times less than an annual license for a proprietary engine—for a mid-size exchange, this translates to savings of $80,000 per year.

Race Condition Protection on Cancel and Fill

Before placing an order we reserve funds: buy limit — price * quantity in quote, sell limit — quantity in base. On cancel we release the reserve. Atomicity is ensured through in-memory balances with asynchronous DB sync. In-memory balance is the source of truth for trading; the DB is for persistence and UI. All balance operations are performed under a mutex, preventing race conditions.

Data Model

CREATE TABLE orders ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id BIGINT NOT NULL REFERENCES users(id), pair_id SMALLINT NOT NULL, side SMALLINT NOT NULL, type SMALLINT NOT NULL, status SMALLINT NOT NULL DEFAULT 0, 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() ); CREATE INDEX idx_orders_user_status ON orders(user_id, status) WHERE status IN (0, 1); CREATE INDEX idx_orders_pair_side_price ON orders(pair_id, side, price) WHERE status IN (0, 1); 

Critical point: the matching engine works in memory; the DB is only for persistence. DB writes are asynchronous via a queue.

Stop Orders and Trigger Mechanism

Stop orders are stored in a separate structure—sorted by stop price. On each trade the matching engine publishes the last price. The stop orders processor subscribes to price updates:

func (sp *StopProcessor) OnPriceUpdate(pair string, lastPrice decimal.Decimal) { triggeredBuys := sp.buyStops.GetTriggered(pair, lastPrice) triggeredSells := sp.sellStops.GetTriggered(pair, lastPrice) for _, stop := range append(triggeredBuys, triggeredSells...) { sp.activateStop(stop, lastPrice) } } 

Trailing stop is a special case. When the price moves in the user's favor, the stop price is recalculated. Implementation via event-driven recalculation on each trade.

Trailing stop implementation details

A trailing stop is a dynamic stop order whose trigger price follows the market at a fixed distance. Algorithm: on each price update, if the price moves in the client's favor, the stop price is recalculated: new_stop_price = current_market_price - distance for sell trailing stop. On a move against the client, the stop price remains unchanged, locking in profit. In implementation we use a priority queue by stop_price, updated on each trade.

Decimal Precision and Floating Point

Never use float64 for financial calculations. 0.1 + 0.2 != 0.3 in IEEE 754. We use: Go — shopspring/decimal, Python — decimal.Decimal, Java — BigDecimal, JavaScript — decimal.js. All stored values are NUMERIC(36,18). Precision and scale are set per trading pair (Bitcoin: 8 digits, meme coins: up to 18).

Implementation Stages

  1. Design — requirements analysis, algorithm selection, load modeling up to 100,000 orders/sec.
  2. Development — writing the matching engine from scratch or based on reference architecture (Go, btree, decimal).
  3. Integration — connection with PostgreSQL, setup of asynchronous writes and balance module.
  4. Testing — unit tests (coverage >85%), property-based testing (fuzzing), load testing with latency/throughput measurements.
  5. Deployment — infrastructure setup, monitoring, team training.

What's Included

When you order, you receive:

  • Architectural documentation of the matching engine and API specification
  • Source code repository (Go, production-ready)
  • Unit test and integration test suite (coverage >85%)
  • Load testing results with latency/throughput metrics
  • Deployment and operations guide
  • 2 months of post-production support and team training

The total investment for a production-ready system starts at $60,000.

Testing

The matching engine is covered with unit tests for edge cases:

  • Partial fill with remainder
  • FOK with insufficient liquidity
  • IOC with partial fill
  • Simultaneous cancel and fill (race condition)
  • Stop order triggers at the moment of placement
  • Decimal overflow on extreme values

Property-based testing (fuzzing) — random sequences of orders are generated, invariants are checked: total volume bought equals total volume sold, balances match.

Development Timelines

  • MVP (limit + market, no stop, no time-in-force): 3–4 weeks (estimate: $20,000–$30,000)
  • Full system with stop orders, all TIF modifiers, trailing stop: 8–12 weeks (typical cost: $40,000–$80,000)
  • Production-ready with audit, load tests, monitoring: +4–6 weeks

The project budget is calculated individually based on your requirements. Get a consultation for your project—contact us for a preliminary estimate. You can also order an audit of your current architecture—we will provide a report with recommendations.