Rust & CEX Engineering — OKX Interview Preparation
Last updated: 2026-03-29
Todo:
- Tokyo
- Borrow checker
- async/await (Future Tait, method)
- Smart pointers, Box Arc, RefCell
- mutex, guards, RvLock
- Send and Sync Trait - и что он дает при пересылке в другой thread
- lifetimes
- Impl, dyn,
- [ ]
Resources
Rust Fundamentals
- The Rust Book — official, zero to idiomatic Rust
- Rust by Example — runnable examples for every concept
- Rustlings — fix small exercises locally, builds muscle memory fast
- Rust Reference — language spec, types, semantics
- Async Book — Futures, async/await, the async ecosystem
- Tokio Tutorial — official step-by-step, build mini projects with Tokio
Practice & Challenges
- Exercism Rust Track — 100+ exercises with mentored feedback
- LeetCode — algorithm bank with full Rust support
- Advent of Code — annual puzzles, huge Rust community, great for benchmarking idiomatic solutions
- CodeCrafters — build Redis, Git, HTTP server from scratch in Rust
- Rust Quiz — tricky edge-case questions by dtolnay (author of serde/syn)
Order Book & Matching Engine
- matching-engine-rs — ITCH order book processing in Rust, actively maintained
- OrderBook-rs — high-performance lock-free order book for low-latency use
- GitHub: matching-engine (Rust) — all public Rust matching engine repos sorted by stars
- Engineering Low-Latency Trading in Rust — real production systems, P50/P99 latency, design tradeoffs
- Rust vs C++ for Trading Systems — Databento engineering blog, latency-critical infrastructure comparison
- Nanosecond Benchmarking for HFT in Rust — CPU-timestamp benchmarking for ultra-low-latency trading
- Intro to Async Rust & Tokio Architecture — Tokio scheduler, I/O driver, async task model — directly applicable to exchange WS work
Algorithms & Data Structures
- TheAlgorithms/Rust — classic algorithms in idiomatic Rust
- EbTech/rust-algorithms — competitive-programming DSA, concise and well-annotated
- RustBook (QMHTMY) — book-style Rust DSA reference
- BTreeMap stdlib docs — the key structure for price-sorted order book sides
Table of Contents
- 1) Role & what OKX is looking for
- 2) What the role is in practice
- 3) Four layers of a CEX you must speak to
- 4) Rust fundamentals refresh
- 5) Exchange domain knowledge
- 6) Key crates to know
- 7) System design questions
- 8) Rust interview Q&A
- 9) Exchange domain Q&A
- 10) Build project idea — mini matching engine
- 11) Crash plan — 7 days
- 12) Must-know shortlist
1) Role & what OKX is looking for
Target role: Staff/Senior Rust Software Engineer at OKX
What OKX explicitly asks for
- Deep Rust expertise — language internals, ecosystem, toolchain
- Cross-platform FFI bindings and system integration
- Performance tuning in high-throughput / low-latency systems
- Preferred: prior experience in trading systems or fintech platforms
- Blockchain / cryptographic protocol experience (bonus)
- Open-source Rust contributions (bonus)
- Mentorship and architectural ownership at scale
Why Rust at a CEX
Deterministic latency without GC pauses + memory safety enforced at compile time. The borrow checker eliminates data races and use-after-free — exactly the bugs that cause production incidents in financial systems.
One production Rust matching engine: P50 tick-to-trade = 4.2μs, P99 = 7.8μs — matching legacy C++ baselines.
2) What the role is in practice
This is a systems engineering role, not application CRUD. OKX wants engineers who can:
- Build or extend a matching engine (ordering, fills, state)
- Design market data pipelines (WebSocket fan-out, sequence gaps)
- Write risk engine components (pre-trade checks, position counters)
- Reason about latency at microsecond level
- Own correctness across concurrent systems
Behavioral framing for senior-level questions:
“I approach exchange systems as four layers: order gateway, matching core, market data distribution, and risk. Each has different latency and correctness requirements. In Rust this maps naturally: single-threaded matching core fed by MPSC channels, async I/O at the gateway, lock-free atomics in risk counters, broadcast channels for market data fan-out.”
3) Four layers of a CEX you must speak to
| Layer | What it does | Rust approach |
|---|---|---|
| Order Gateway | Receives orders (REST/WebSocket/FIX), validates, routes | Tokio async, session management, FIX protocol |
| Matching Engine | Matches buy/sell orders, produces fills | Single-threaded, MPSC input, BTreeMap order book |
| Market Data | Publishes book updates + trades to subscribers | Tokio broadcast channel, sequence numbers, snapshot + delta |
| Risk Engine | Pre-trade checks (position limits, balance), post-trade settlement | AtomicI64 counters, RwLock account state, <1μs target |
4) Rust fundamentals refresh
Ownership & Borrowing
- Single owner at a time; borrow checker enforces at compile time
&Tshared reference (many readers),&mut Texclusive (one writer)- Move semantics: assignment transfers ownership unless
Copy - Lifetimes: compiler proves references don’t outlive data
Exchange relevance: explicit ownership makes shared-state bugs impossible to compile. No GC = no latency spikes.
Rc vs Arc
Rc<T>— reference counted, single-thread onlyArc<T>— atomic reference counted, safe across threads (Send + Sync)Arc<Mutex<T>>for shared mutable state across threads
Send and Sync
Send: type can be moved to another threadSync: type can be shared by reference across threads- Tokio’s
spawnrequires futures to beSend(work-stealing scheduler)
Async / Tokio channels
// Market data fan-out — N subscribers each get every message
let (tx, _) = tokio::sync::broadcast::channel(1024);
// Order flow into matching engine — single consumer
let (order_tx, mut order_rx) = tokio::sync::mpsc::channel(256);
// BBO — single writer, many readers, only latest value
let (bbo_tx, bbo_rx) = tokio::sync::watch::channel(initial_bbo);
// Race order response vs timeout
tokio::select! {
result = order_future => handle_result(result),
_ = tokio::time::sleep(timeout) => handle_timeout(),
}| Channel | Use case |
|---|---|
mpsc | Order flow into matching engine (many senders, one receiver) |
broadcast | Market data to N subscribers |
watch | BBO / last price (single writer, many readers, only latest) |
oneshot | Request/response pattern |
Concurrency gotchas
- False sharing: two
AtomicI64on same 64-byte cache line → cache invalidation storms → use#[repr(align(64))] - Ordering correctness:
Relaxedfor independent counters,AcqRelfor producer/consumer pairs,SeqCstfor total ordering (expensive) - Mutex vs RwLock:
RwLockfor read-heavy account state;Mutexwhen writes are frequent crossbeam::channeloverstd::sync::mpscin hot paths
Error handling
// Domain errors — callers can match on variants
#[derive(thiserror::Error, Debug)]
pub enum OrderError {
#[error("insufficient balance: need {need}, have {have}")]
InsufficientBalance { need: u64, have: u64 },
#[error("invalid price: {0}")]
InvalidPrice(Decimal),
}
// Never in production hot paths — panics
some_option.unwrap()Performance patterns
- Arena allocation:
bumpalo— allocate at startup, reset between cycles, zero per-order heap alloc - Avoid
Box<T>in hot paths — prefer fixed-size stack structs - Zero-copy serde:
rkyvorflatbuffersfor wire format - Huge pages: 2MB pages reduce TLB misses ~99%
- Profiling:
cargo-flamegraph,criterionfor benchmarks,perffor CPU counters
5) Exchange domain knowledge
Order book
use std::collections::{BTreeMap, VecDeque};
struct OrderBook {
bids: BTreeMap<Price, VecDeque<Order>>, // sorted descending
asks: BTreeMap<Price, VecDeque<Order>>, // sorted ascending
}
// Best bid = bids.iter().next_back()
// Best ask = asks.iter().next()- Insert/delete: O(log n)
- Lock-free variant: per-price-level atomics, no global lock
Matching engine — sequencer pattern
Client A ──┐
Client B ──┼──► mpsc::channel ──► Single-threaded matcher ──► Fill events
Client C ──┘ └──► broadcast
- All orders enter through one MPSC channel
- Matching core is single-threaded — no locks needed inside
- Deterministic: same input → same output (required for audit/replay)
Order types
| Type | Behaviour |
|---|---|
| Limit | Rest on book at specified price |
| Market | Fill immediately at best available price |
| IOC (Immediate or Cancel) | Fill what’s available, cancel remainder |
| FOK (Fill or Kill) | Fill entire qty or cancel entirely |
| Post-Only | Reject if would take liquidity |
| Stop / Stop-Limit | Triggered when price crosses level |
Market data — sequence gaps
- Subscribe to WebSocket delta stream
- Receive snapshot with
seq=1000 - Apply deltas with
seq > 1000in order - If gap detected → re-fetch snapshot, buffer out-of-order deltas
- Each update format:
{seq, bids: [[price, qty]], asks: [[price, qty]]}
Risk engine
struct RiskEngine {
positions: DashMap<AssetId, AtomicI64>, // lock-free reads
balances: Arc<RwLock<HashMap<UserId, Balance>>>, // read-heavy
}
impl RiskEngine {
fn pre_trade_check(&self, order: &Order) -> Result<(), RiskError> {
// Target: <1μs — no heap alloc, no I/O
let pos = self.positions
.get(&order.asset)
.map(|p| p.load(Ordering::Acquire))
.unwrap_or(0);
if pos + order.qty > MAX_POSITION {
return Err(RiskError::PositionLimitExceeded);
}
Ok(())
}
}FIX protocol
- Industry standard for institutional order flow
- Logon → Heartbeat → New Order Single → Execution Report
- Session-level: sequence numbers, resend request on gap
- Rust crates:
fix-rs,quickfix-rs
WebSocket server pattern
let (market_tx, _) = broadcast::channel::<MarketUpdate>(4096);
async fn handle_ws_client(
stream: WebSocketStream<TcpStream>,
mut market_rx: broadcast::Receiver<MarketUpdate>,
) {
loop {
tokio::select! {
update = market_rx.recv() => { /* send to client */ }
msg = stream.next() => { /* handle subscribe/unsubscribe */ }
}
}
}6) Key crates to know
| Crate | Purpose |
|---|---|
tokio | Async runtime — the standard for network services |
tokio-tungstenite | WebSocket server/client on Tokio |
serde / serde_json | Serialization — REST API payloads |
rkyv / flatbuffers | Zero-copy serialization for hot paths |
crossbeam | Better channels + epoch-based GC for lock-free structures |
dashmap | Concurrent HashMap without global lock |
parking_lot | Faster Mutex/RwLock than std |
bumpalo | Arena allocator — zero heap alloc per order |
thiserror | Typed error enums for domain code |
anyhow | Error propagation in application/binary code |
criterion | Micro-benchmarking |
tracing | Structured async-aware logging |
prometheus | Metrics export |
rust_decimal | Exact decimal arithmetic for prices/quantities |
axum | REST API server (Tokio-native) |
7) System design questions
Design a limit order book
- Data structure:
BTreeMap<Price, VecDeque<Order>>per side - Operations: insert O(log n), cancel O(log n), match O(1) at top of book
- Concurrency: single-threaded core fed by MPSC — no locks inside matcher
- Persistence: WAL (write-ahead log) on every order event for crash recovery
- Market data: emit delta on every mutation with sequence number
Design a matching engine handling 100k orders/sec
- Sequencer pattern: MPSC channel as the serialization point
- Pre-allocate order pool at startup (arena allocator)
- Avoid heap allocation in hot path
- Single thread pinned to isolated CPU core
- Async I/O on separate Tokio threads
- Benchmark with
criterion, profile withcargo-flamegraph
Handle WebSocket sequence gaps
Subscribe → receive REST snapshot (seq=N) → apply deltas with seq > N → on gap: re-fetch snapshot → resume. Buffer out-of-order deltas during snapshot fetch, discard those with seq ⇐ snapshot seq.
Sub-microsecond risk check
AtomicI64 per position, compare_exchange for CAS-based reservation. No heap alloc. No I/O. Inline the hot path. Pin thread to core. Use Ordering::AcqRel for producer/consumer pairs.
8) Rust interview Q&A
Q: What is the borrow checker and why does it matter for trading systems? The borrow checker enforces at compile time that references don’t outlive data and mutable access is exclusive. For trading systems: data races are impossible to compile — no corrupted order state, no use-after-free. No GC = no latency spikes.
Q: When do you use Arc<Mutex<T>> vs channels?
Arc<Mutex<T>> for state multiple components read/write (account balances). Channels for one-directional message passing where the receiver owns state exclusively — prefer channels in the matching engine core because the sequencer pattern eliminates lock contention entirely.
Q: Explain Send and Sync. When does Tokio care?
Send: value can be moved to another thread. Sync: &T can be shared across threads. Tokio’s spawn requires futures to be Send (work-stealing scheduler may run them on any thread). Rc<T> is not Send — use Arc<T> across threads.
Q: async fn vs spawn_blocking — when?
async fn for I/O-bound work (network, disk). spawn_blocking for CPU-bound work that would block the async executor (heavy crypto, order replay). Blocking the executor starves other tasks.
Q: How do you avoid allocations in a hot path?
Pre-allocate a pool at startup with bumpalo or a fixed-size ring buffer. Reuse memory per-order. Use value types on the stack. Avoid Box<T>, Vec growth, String in the matching loop. Verify with cargo-flamegraph.
Q: What is false sharing and how do you fix it?
Two fields on the same 64-byte cache line cause cache invalidation storms when written from different threads. Fix with #[repr(align(64))] to force each field to its own cache line.
Q: thiserror vs anyhow?
thiserror for domain/library code — typed errors callers can match on. anyhow for application code — ergonomic ? propagation without defining types. Domain layers use thiserror, the binary’s main uses anyhow.
Q: parking_lot::Mutex vs std::sync::Mutex?
parking_lot is faster (no poisoning, smaller, better contention handling). Use in performance-sensitive code. std::sync::Mutex in public library APIs for compatibility.
Q: How does Tokio’s work-stealing scheduler work? Each worker thread has a local task queue. When idle, it steals from other threads. Tasks are polled when their Future is woken (I/O ready, timer fired, channel message). Never block the executor with CPU-heavy work.
9) Exchange domain Q&A
Q: What’s the difference between IOC and FOK? IOC fills what’s available at the limit price and cancels the remainder. FOK requires the entire quantity to be filled immediately or the whole order is cancelled — no partial fills.
Q: How does price-time priority work?
Orders at the same price level are matched in arrival order (FIFO). Better price always takes priority over time. BTreeMap<Price, VecDeque<Order>> — the deque gives FIFO within a price level.
Q: What is a mark price and why does it matter? An index-derived fair value price used for liquidations and unrealized PnL, resistant to manipulation on any single venue. OKX uses a composite index from multiple sources. Critical for perpetual futures — liquidation price is based on mark, not last trade.
Q: How do you handle duplicate orders?
Client sends a client_order_id (idempotency key). Gateway deduplicates in a short-lived LRU cache keyed by (user_id, client_order_id). If seen, return the original result without re-processing.
Q: What is a circuit breaker in exchange context? Automatic halt when price moves beyond a threshold in a short window (e.g., >5% in 1 minute). Prevents cascading liquidations. Exchange-level: halt matching. Product-level: widen spread, reject aggressive orders.
Q: What is a WAL and why do matching engines need it? Write-Ahead Log — every order event is durably written to disk before being applied to in-memory state. On crash, replay the WAL to reconstruct exact state. Required for audit trails and deterministic crash recovery.
10) Build project idea — mini matching engine
What to build
A limit order book matching engine with:
- REST API to submit/cancel orders (
axum) - WebSocket market data feed with sequence numbers (
tokio-tungstenite) - MPSC sequencer pattern — single-threaded matching core
- Pre-trade risk check (balance/position limits)
- Structured logging (
tracing) + metrics (prometheus) criterionbenchmarks showing throughput
Step-by-step
| Step | What it proves |
|---|---|
OrderBook struct with BTreeMap | Core data structure knowledge |
| Matching logic (limit + market + IOC) | Domain understanding |
| MPSC sequencer + Tokio runtime | Async + concurrency patterns |
| Axum REST endpoints (submit/cancel/status) | API layer |
| WS feed with sequence numbers | Market data pipeline |
| AtomicI64 risk checks | Low-latency systems thinking |
criterion benchmarks | Performance engineering mindset |
Repo structure
mini-exchange/
src/
order_book.rs # BTreeMap-based book + matching
risk.rs # AtomicI64 position checks
gateway.rs # Axum REST handlers
market_data.rs # broadcast channel + WS server
main.rs # Tokio runtime, wiring
benches/
matching.rs # criterion benchmark
11) Crash plan — 7 days
| Day | Focus | Goal |
|---|---|---|
| 1 | Rust refresh — ownership, lifetimes, traits, error handling | Can explain borrow checker fluently |
| 2 | Async Rust — Tokio, channels (mpsc/broadcast/watch), select! | Can design async pipeline from memory |
| 3 | Concurrency — atomics, Mutex vs RwLock, false sharing, crossbeam | Can reason about data races and cache behaviour |
| 4 | Order book — implement BTreeMap-based book + matching logic | Working code to walk through in interview |
| 5 | CEX domain — order types, FIX, risk engine, market data gaps | Can answer all domain Q&A above |
| 6 | System design — matching engine 100k/s, WS fan-out, risk checks | Can whiteboard any of the 4 designs cold |
| 7 | Mock interview — explain every section aloud, timed | Comfortable pace, no hesitation on Rust fundamentals |
12) Must-know shortlist
If you have 1 day, know these cold:
Rust
- Ownership / borrowing / lifetimes in one sentence each
-
Arc<Mutex<T>>vs channels — when to use which -
Send + Sync— what they mean and why Tokio needs them -
async fnvsspawn_blocking -
thiserrorvsanyhow - Atomic
Ordering—RelaxedvsAcqRelvsSeqCst - False sharing — what it is and how to fix it
-
parking_lotvsstd::sync::Mutex
Exchange domain
- Order book data structure + matching algorithm
- Sequencer/MPSC pattern for matching engine
- IOC vs FOK vs Post-Only
- Price-time priority (FIFO within price level)
- WebSocket delta stream + sequence gap recovery
- Pre-trade risk check design (AtomicI64, <1μs)
- Mark price purpose (liquidations, PnL)
- WAL for crash recovery + audit trail
Crates to name-drop
-
tokio,axum,tokio-tungstenite -
crossbeam,dashmap,parking_lot -
thiserror,anyhow,tracing -
criterion,bumpalo,rust_decimal