Order System Development: Limit, Market, Stop

We design and develop full-cycle blockchain solutions: from smart contract architecture to launching DeFi protocols, NFT marketplaces and crypto exchanges. Security audits, tokenomics, integration with existing infrastructure.
Showing 1 of 1All 1305 services
Order System Development: Limit, Market, Stop
Complex
~1-2 weeks
Frequently Asked Questions

Blockchain Development Services

Blockchain Development Stages

Latest works

  • image_website-b2b-advance_0.webp
    B2B ADVANCE company website development
    1349
  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1247
  • image_websites_belfingroup_462_0.webp
    Website development for BELFINGROUP
    949
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1183
  • image_logo-advance_0.webp
    B2B Advance company logo design
    642
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    921

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.

Why exchange development requires deep domain expertise

We develop exchanges — not 'chart sites,' but matching engines that process thousands of orders per second without delay, route liquidity between pools, and guarantee that no user gains access to others' funds. Teams that start with the UI and postpone the engine 'for later' end up rewriting everything in six months in 90% of cases.

Order Book vs AMM: where most projects break

Centralized exchanges (CEX) are built around an order book + matching engine. Decentralized exchanges (DEX) either also use an order book (dYdX on StarkEx, Serum/OpenBook on Solana) or an AMM with concentrated liquidity (Uniswap v3/v4, Curve, Balancer). A classic mistake when developing a CEX is implementing the matching engine on top of a relational database with transactions for each match. PostgreSQL handles ~500 RPS without special effort, but at peak loads of 5,000–10,000 orders per second, it turns into a deadlock nightmare. The correct architecture: in-memory order book (Redis Sorted Sets or custom C++/Rust structure), asynchronous writing of matches to PostgreSQL via a queue (Kafka/RabbitMQ), and a separate settlement service that finally updates balances.

For DEX, the most painful problem is sandwich attacks and MEV. A pool with a plain xy=k AMM without slippage protection becomes a target for MEV bots within hours of launch. Uniswap v2 lost hundreds of millions of dollars in user liquidity. Solutions: integration with Flashbots Protect, a commit-reveal scheme for orders, or switching to TWAMM (Time-Weighted AMM) for large trades.

Concentrated liquidity and impermanent loss

Uniswap v3 introduced concentrated liquidity – LPs choose a price range in which to provide liquidity. Capital efficiency increased 4,000x compared to v2 for stable pairs. But implementing this mechanism correctly is non-trivial. The Uniswap v3 liquidity contract uses tick-based accounting: the price space is divided into discrete ticks (tick = log₁.0001(price)), each tick stores accumulated fee growth and liquidity delta. When creating a position, the lower and upper ticks are computed, and the contract recalculates all active positions at each swap. Storage layout is critical here – incorrect variable packing in slots easily adds 40–60% to swap gas cost.

We implemented a Uniswap v3 fork for a client on Polygon with a custom fee tier system. The initial version consumed 180k gas for a swap across 2 ticks. After slot packing of variables in Tick.Info and inlining several internal calls, it dropped to 112k gas. This reduced gas costs by 38% and saved the client substantial costs on fees monthly. The techniques applied are described in the Uniswap v3 Whitepaper and confirmed by our audit experience.

How a matching engine delivers performance

A production-ready matching engine is built according to the following scheme:

  • Order ingestion layer – WebSocket gateway (Go or Rust), accepts orders, validates signature, checks balance via Redis, queues them. Latency at this level must be <1ms.
  • Matching core – single-threaded event loop (eliminates race conditions without mutexes). In memory, we hold two Sorted Sets for each trading instrument: bids and asks. FIFO matching for limit orders, immediate-or-cancel for market orders. Throughput with a proper Rust implementation – 500k–1M matches per second on a single core.
  • Settlement service – reads matches from Kafka, atomically updates balances in PostgreSQL (UPDATE accounts SET balance = balance - $1 WHERE id = $2 AND balance >= $1). Optimistic locking via row versioning.
  • Withdrawal pipeline – separate service with cold/hot wallet architecture. The hot wallet holds 5–10% of total deposits, the rest is cold storage with multi-sig (Gnosis Safe or custom HSM). Automatic withdrawals only from hot wallet, large amounts require manual authorization.
Component Technology Latency / Throughput
Order gateway Go + WebSocket <1ms p99
Matching engine Rust (in-memory) 500k+ orders/sec
Balance store Redis (write-through) <0.5ms
Settlement DB PostgreSQL 14+ ~50k TPS with partitioning
Event streaming Apache Kafka 1M+ events/sec
Blockchain node Geth / Solana validator depends on chain

How our exchange development process ensures reliability

Smart contracts and gas optimization

For EVM-based DEX (Ethereum, Arbitrum, Optimism, Polygon), the entire critical path lives in Solidity. Main contracts: Pool, Factory, Router, PositionManager (for v3-like), and Quoter for off-chain calculations. Typical mistakes we see in audits:

Reentrancy via callback. Uniswap v3 uses flash swap with a callback (uniswapV3SwapCallback). If your router lacks a nonReentrant guard and you don't check msg.sender == pool, the contract gets drained via a nested call. This is not hypothetical – several v3 forks lost funds this way.

Oracle manipulation in AMM. If your contract uses the spot price from the pool for collateral calculation, it is front-runnable. Correct: TWAP over 30+ minutes (Uniswap v3 OracleLib) or an external oracle (Chainlink).

Unbounded loops in liquidity range. If a swap crosses many ticks in a row (price impact 80%+), gas may exceed the block limit. Need MAX_TICKS_CROSSED with partial fill and returning the remainder.

For Solana DEX (Anchor framework, Rust), the architecture is fundamentally different: account-based model, Program Derived Addresses (PDA) instead of storage, Cross-Program Invocations instead of internal calls. Solana's throughput (~3,000–4,000 TPS vs 15–30 on Ethereum mainnet) allows building on-chain order books – exactly what Phoenix DEX does.

Liquidity bootstrapping and aggregator integration

Launching a pool is not enough – you need to ensure liquidity at launch. Practical mechanisms:

  • Liquidity Bootstrapping Pool (LBP) – initial price is high, asset weights dynamically shift, creating selling pressure and even token distribution. Implemented in Balancer v2.
  • Initial Liquidity Offering via Uniswap v3 – adding liquidity in a narrow range around the initial price, then gradually expanding as volume grows. Requires active liquidity management or integration with Arrakis/Gamma.
  • Integration with 1inch, Paraswap, Li.Fi – aggregators bring traffic but require standard compliance: the pool must have correct getAmountsOut, support ERC-20 approval/permit, and not have custom transfer hooks that break the aggregator's routing.

Development process and deliverables

Analytics and design begin with choosing the architectural model: CEX with custodial storage, non-custodial DEX, or hybrid (off-chain order book + on-chain settlement, like dYdX v3). This decision determines everything – regulatory load, tech stack, team.

Development proceeds in layers: first smart contracts with full Foundry coverage (fuzzing, invariant testing), then backend services, then integration layer, and finally frontend. Testing includes fork testing on mainnet via Foundry – we reproduce real liquidity conditions, not synthetic ones.

Audit is mandatory before mainnet deployment. For DEX contracts, minimally one firm with manual review (Trail of Bits, Spearbit, Code4rena contest). For CEX custody, audit of key storage processes. We guarantee all contracts undergo formal verification and fuzzing testing (Echidna, Foundry invariant).

Estimated timelines

Exchange type Timeframe
DEX (AMM, xy=k) 3 to 5 months
DEX with concentrated liquidity (v3-like) 6 to 10 months
CEX (matching engine + custody + trading UI) 8 to 14 months
Integration with existing protocol 4 to 8 weeks

Cost is calculated individually after a technical briefing: chain selection, throughput requirements, custodial model. Our certified engineers with 10+ years of experience will help you choose the optimal architecture and avoid common pitfalls. Contact our team for a detailed proposal.

Pitfalls to avoid at launch

  • Forgetting the price oracle in AMM. Spot price can be manipulated with a flash loan in one transaction. If your lending protocol uses the spot price from its own pool, that's a bug.
  • Hot wallet without limits. A CEX without daily limits on automatic withdrawals is an invitation for attackers. Compromising one key should lose at most 10% of total funds.
  • Absence of circuit breaker. A 40% price drop in 5 minutes should halt automatic liquidations or withdrawals until manual review. Without this, a cascading liquidation spiral destroys all TVL.
  • Incorrect decimal handling. USDC uses 6 decimals, WBTC – 8, most tokens – 18. Mixing without normalization leads to either precision loss or overflow. Solidity has no float; we work with fixed-point using FullMath (mulDiv with overflow protection).

Want to avoid these problems? Get a consultation — we will select the architecture for your project and provide exact timelines. Order exchange development with quality guarantee and ongoing support.