The Order Service is a NestJS (TypeScript) microservice responsible for the complete order lifecycle — from intake and validation through risk/wallet checks to publishing commands for the matching engine and processing execution reports.
Order Intake & Validation : Accept order requests via REST API, validate against instrument rules and schema constraints.
Risk & Session Checks : Call Risk Service for pre-trade checks (leverage, margin, exposure limits).
Wallet Integration : Call Wallet Service to lock margin before accepting orders, unlock on cancel/reject, settle on fill.
Lifecycle Persistence : Persist every order state transition to PostgreSQL with a full event-sourced audit trail (order_events table).
Command Publishing : Publish order.command.v1 to Kafka for the matching engine.
Execution Processing : Consume engine.event.v1 from the matching engine and update order states.
Idempotency : Deduplicate order submissions via idempotency_key (Redis + DB).
The Order Service is the gateway between the client and the matching engine. It ensures orders are valid, funded, and risk-checked before entering the order book.
graph TB
Client[Client App] -->|REST| OrderAPI[Order Service API]
OrderAPI -->|gRPC| Risk[Risk Service]
OrderAPI -->|gRPC| Wallet[Wallet Service]
OrderAPI -->|gRPC| Metadata[Metadata Service]
OrderAPI -->|Kafka| ME[Matching Engine]
ME -->|Kafka| OrderAPI
OrderAPI -->|Stores| PG[(PostgreSQL)]
OrderAPI -->|Cache| Redis[(Redis)]
Field Type Description idUUID (PK) Account ID user_idUUID Owner user account_typeENUM individual, institutional, system_market_makerstatusENUM active, suspended, closedcreated_atTIMESTAMPTZ Creation time
Field Type Description idUUID (PK) Instrument ID symbolVARCHAR(32) UNIQUE Trading symbol (e.g., BTCUSDT-PERP) base_currencyVARCHAR(10) Base asset quote_currencyVARCHAR(10) Quote asset tick_sizeNUMERIC(24) Minimum price increment lot_sizeNUMERIC(24) Minimum quantity increment min_notionalNUMERIC(24) Minimum order notional value max_leverageINT Maximum allowed leverage statusENUM active, halted, delisted
Field Type Description idUUID (PK) Order ID client_order_idTEXT Client-provided tracking ID account_idUUID (FK) References accounts instrument_idUUID (FK) References instruments sideENUM(buy, sell) Order side typeENUM(limit, market, stop_limit, stop_market) Order type time_in_forceENUM(gtc, ioc, fok, gtd) Time-in-force policy priceNUMERIC(24) Limit price (NULL for market) stop_priceNUMERIC(24) Stop trigger price quantityNUMERIC(24) Original quantity filled_qtyNUMERIC(24) Cumulative filled quantity remaining_qtyNUMERIC(24) Remaining to fill avg_fill_priceNUMERIC(24) Volume-weighted average fill price leverageINT Leverage multiplier (1-100) post_onlyBOOLEAN Reject if would take liquidity reduce_onlyBOOLEAN Only reduce existing position statusENUM Order state machine value idempotency_keyTEXT UNIQUE Deduplication key fee_currencyVARCHAR(10) Currency for fees expires_atTIMESTAMPTZ Expiry for GTD orders created_atTIMESTAMPTZ Order creation time updated_atTIMESTAMPTZ Last status change
Field Type Description idUUID (PK) Event ID order_idUUID (FK) References orders event_typeTEXT created, accepted, partially_filled, filled, canceled, rejected, replacedprev_statusTEXT Previous order status new_statusTEXT New order status filled_qtyNUMERIC(24) Fill quantity for this event fill_priceNUMERIC(24) Fill price for this event trade_idUUID Trade ID (for fill events) reasonTEXT Reason for reject/cancel metadataJSONB Additional event context trace_idTEXT OpenTelemetry trace ID created_atTIMESTAMPTZ Event timestamp
Transactional outbox table for reliable Kafka publishing:
Field Type Description idUUID (PK) Outbox entry ID topicTEXT Target Kafka topic keyTEXT Kafka message key payloadJSONB Serialized message publishedBOOLEAN Whether successfully published created_atTIMESTAMPTZ Entry creation time published_atTIMESTAMPTZ When published
Key Pattern Purpose TTL order:idem:{key}Idempotency deduplication 24 hours order:rate:{account_id}Per-account rate limiter Sliding window instrument:{id}Cached instrument definitions 5 min
new --> accepted --> partially_filled --> filled
rejected cancel_pending filled
accepted --> replace_pending --> replaced
filled / canceled --> expired
From To Trigger newacceptedMatching engine acknowledges order newrejectedValidation failure, risk check failure, or insufficient funds acceptedpartially_filledPartial fill from matching engine acceptedfilledComplete fill from matching engine acceptedcancel_pendingCancel request submitted partially_filledfilledRemaining quantity filled partially_filledcancel_pendingCancel remaining cancel_pendingcanceledCancel confirmed by engine acceptedreplace_pendingReplace request submitted replace_pendingreplacedReplace confirmed by engine filled / canceledexpiredGTD expiry reached
Method Endpoint Description POST/v1/ordersCreate new order GET/v1/ordersList orders (paginated, filterable) GET/v1/orders/:idGet order by ID PUT/v1/orders/:idReplace order (cancel + new) DELETE/v1/orders/:idCancel order
Method Endpoint Description GET/v1/tradesList all trades GET/v1/trades/:instrumentList trades for instrument
Create Order Request:
"clientOrderId" : " my-order-1 " ,
"idempotencyKey" : " unique-key-123 "
Response Format (List):
"data" : [ /* orders */ ],
"pagination" : { "page" : 1 , "limit" : 50 , "total" : 142 }
Topic Description order.command.v1Order commands (new, cancel, replace) sent to matching engine
Topic Description engine.event.v1Execution reports from matching engine (fills, accepts, rejects, cancels)
{ "name" : " command_type " , "type" : " string " },
{ "name" : " order_id " , "type" : " string " },
{ "name" : " account_id " , "type" : " string " },
{ "name" : " instrument_id " , "type" : " string " },
{ "name" : " symbol " , "type" : " string " },
{ "name" : " side " , "type" : " string " },
{ "name" : " order_type " , "type" : " string " },
{ "name" : " quantity " , "type" : " string " },
{ "name" : " price " , "type" : [ " null " , " string " ] },
{ "name" : " time_in_force " , "type" : " string " },
{ "name" : " trace_id " , "type" : " string " }
Auth : Extract x-user-id, x-account-id from headers (set by API Gateway after JWT validation)
Idempotency Check : Check idempotency_key in Redis → if hit, return cached response
Schema Validation : Validate request body with Zod schemas
Instrument Lookup : Fetch instrument from Metadata Service (gRPC, cached in Redis)
Business Validation : Check quantity tick/lot size, min notional, price precision, leverage limits
Risk Check : Call RiskService.PreTradeCheck() via gRPC (leverage, margin, exposure)
Wallet Lock : Call WalletService.LockFunds() via gRPC (reserve margin)
Persist : Insert orders row (status = new) + order_events row + outbox row in single transaction
Publish : Outbox relay publishes order.command.v1 to Kafka
Response : Return order with status new
When engine.event.v1 arrives:
Parse event (fill, cancel, reject)
Load order from DB
Apply state transition
For fills: call WalletService.SettleTrade() to apply P&L and fees
Insert order_events row
Update order status, filled_qty, avg_fill_price
If fully filled: update status to filled
Quantity : Must be positive, divisible by lot_size, does not exceed instrument max
Price : Must be positive (for limit), divisible by tick_size
Notional : price * quantity >= min_notional
Leverage : 1 <= leverage <= instrument.max_leverage
Time-in-Force : Market orders must be ioc or fok; limit orders can be gtc, ioc, fok, gtd
GTD Expiry : expires_at must be in the future (for GTD orders)
Post-Only : Only valid for limit orders
Reduce-Only : Validated against current position
Per-Account : 50 requests/second per account (configurable)
Per-IP : 100 requests/second per IP
Implementation : Redis sliding window counter
order_created_total{side,type} — Orders created by side and type
order_filled_total{side} — Orders filled
order_rejected_total{reason} — Rejections by reason
order_canceled_total — Cancellations
order_latency_ms{stage} — Latency per processing stage
outbox_pending_gauge — Pending outbox messages
Spans: order.create, order.validate, order.risk_check, order.wallet_lock, order.persist, order.publish, order.process_fill
Structured JSON: order_id, account_id, instrument_id, side, type, status, trace_id. Prices and quantities are logged for debugging.
Scenario Handling Risk service unavailable Reject order with RISK_CHECK_UNAVAILABLE Wallet lock fails Reject order with INSUFFICIENT_BALANCE Kafka publish fails Outbox pattern retries; order stays in new until published Engine event processing fails Consumer retries with backoff; idempotent by trade_id DB transaction fails Automatic rollback; client retries with same idempotency key
Metric Target Order acceptance latency (p95) < 100 ms Outbox drain latency (p95) < 500 ms Fill processing latency (p95) < 200 ms Order creation success rate >= 99.5%
Variable Description Default POSTGRES_URLPostgreSQL connection string Required REDIS_URLRedis connection string Required KAFKA_BROKERSKafka broker addresses Required RISK_SERVICE_GRPC_URLRisk service gRPC endpoint Required WALLET_SERVICE_GRPC_URLWallet service gRPC endpoint Required METADATA_SERVICE_GRPC_URLMetadata service gRPC endpoint Required RATE_LIMIT_PER_ACCOUNTMax requests per second per account 50
Language : TypeScript
Framework : NestJS
ORM : Drizzle ORM
Database : PostgreSQL
Cache : Redis
Messaging : Kafka with Avro serialization
gRPC : @nestjs/microservices with gRPC transport
Validation : Zod schemas
Package Manager : Bun