Skip to content

Order Service

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.
  • Market Order Pricing: Call Market Data Service for last-traded price when sizing market orders.
  • 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 via a transactional outbox.
  • Execution Processing: Consume engine.event.v1 from the matching engine and update order states. Per-engine-sequence deduplication via Redis.
  • Idempotency: Deduplicate order submissions via idempotency_key (Redis 24h TTL + DB unique constraint).
  • gRPC Query Surface: Serve internal queries — GetOrdersByUser, ListOrderByUserIds, CancelAllOrdersForPool — on port 50060 for other services.

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 PreTradeCheck| Risk[Risk Service] OrderAPI -->|gRPC LockFunds / SettleTrade| Wallet[Wallet Service] OrderAPI -->|gRPC Instrument lookup| Metadata[Metadata Service] OrderAPI -->|gRPC LTP for market orders| MarketData[Market Data Service] OrderAPI -->|order.command.v1| ME[Matching Engine] ME -->|engine.event.v1| OrderAPI OrderAPI -->|Stores| PG[(PostgreSQL)] OrderAPI -->|Idempotency · dedup · cache| Redis[(Redis)] Other[Other Services] -->|gRPC OrderQueryService :50060| OrderAPI
FieldTypeDescription
idUUID (PK)Account ID
user_idUUIDOwner user
account_typeENUMindividual, institutional, system_market_maker
statusENUMactive, suspended, closed
created_atTIMESTAMPTZCreation time
FieldTypeDescription
idUUID (PK)Instrument ID
symbolVARCHAR(32) UNIQUETrading 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_leverageINTMaximum allowed leverage
statusENUMactive, halted, delisted
FieldTypeDescription
idUUID (PK)Order ID
client_order_idTEXTClient-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
leverageINTLeverage multiplier (1-100)
post_onlyBOOLEANReject if would take liquidity
reduce_onlyBOOLEANOnly reduce existing position
statusENUMOrder state machine value
idempotency_keyTEXT UNIQUEDeduplication key
fee_currencyVARCHAR(10)Currency for fees
expires_atTIMESTAMPTZExpiry for GTD orders
created_atTIMESTAMPTZOrder creation time
updated_atTIMESTAMPTZLast status change
FieldTypeDescription
idUUID (PK)Event ID
order_idUUID (FK)References orders
event_typeTEXTcreated, accepted, partially_filled, filled, canceled, rejected, replaced
prev_statusTEXTPrevious order status
new_statusTEXTNew order status
filled_qtyNUMERIC(24)Fill quantity for this event
fill_priceNUMERIC(24)Fill price for this event
trade_idUUIDTrade ID (for fill events)
reasonTEXTReason for reject/cancel
metadataJSONBAdditional event context
trace_idTEXTOpenTelemetry trace ID
created_atTIMESTAMPTZEvent timestamp

Transactional outbox table for reliable Kafka publishing:

FieldTypeDescription
idUUID (PK)Outbox entry ID
topicTEXTTarget Kafka topic
keyTEXTKafka message key
payloadJSONBSerialized message
publishedBOOLEANWhether successfully published
created_atTIMESTAMPTZEntry creation time
published_atTIMESTAMPTZWhen published
Key PatternPurposeTTL
order:idem:{key}Idempotency deduplication24 hours
order:rate:{account_id}Per-account rate limiterSliding window
instrument:{id}Cached instrument definitions5 min
new --> accepted --> partially_filled --> filled
| | |
v v v
rejected cancel_pending filled
|
v
canceled
accepted --> replace_pending --> replaced
filled / canceled --> expired
FromToTrigger
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

All POST / PUT / DELETE endpoints return HTTP 202 Accepted with { orderId, status, message } — order processing is asynchronous via the outbox + matching engine.

MethodEndpointDescription
POST/v1/ordersCreate new order (202)
GET/v1/ordersList orders (paginated / filterable / cursor-based)
GET/v1/orders/:idGet order by ID
PUT/v1/orders/:idReplace / amend order (202)
DELETE/v1/orders/:idCancel order (202)

GET /v1/orders supports: page, limit, status, type, symbol, fromDate, toDate, search, cursor. Results are ordered by updatedAt DESC and the list response is 60s cached in Redis (counts 5 min).

MethodEndpointDescription
GET/v1/tradesList all trades for the authenticated user
GET/v1/trades/:symbolList trades for a specific symbol
MethodEndpointDescription
GET/healthHealth check (no auth)
/admin/*Operational endpoints (instrument refresh, etc.)
/docs, /docs-jsonSwagger UI + OpenAPI spec

All authenticated endpoints require headers set by the upstream API gateway after JWT validation:

HeaderRequiredPurpose
x-user-idyesUser UUID
x-account-idyesTrading account UUID
x-sourcenoRequest source: api (default), web, mobile, broker, market_maker
x-idempotency-keynoDeduplication key for write operations

Create Order Request (POST /v1/ordersCreateOrderDto, validated with class-validator):

{
"symbol": "BTCUSDT-PERP",
"side": "buy",
"type": "limit",
"quantity": "1.5",
"price": "50000.00",
"timeInForce": "gtc",
"leverage": 10,
"postOnly": false,
"reduceOnly": false,
"clientOrderId": "my-order-1",
"idempotencyKey": "unique-key-123",
"feeCurrency": "USDT"
}

All financial fields are decimal strings (never JSON numbers) per the platform’s precision rules.

Response Format (List):

{
"data": [ /* orders */ ],
"pagination": { "page": 1, "limit": 50, "total": 142 }
}
TopicDescription
order.command.v1Order commands (new, cancel, replace) sent to matching engine
TopicDescription
engine.event.v1Execution reports from matching engine (fills, accepts, rejects, cancels)

Order Service also serves an inbound gRPC API used by other internal services. Service name: OrderQueryService.

RPCPurposeCalled By
GetOrdersByUser(user_id, account_id)All orders for one user / accountRisk, Settlement
ListOrderByUserIds(user_ids[])Batch lookup across usersRisk, Reporting
CancelAllOrdersForPool(pool_id, allocation_id, trace_id)Bulk-cancel for a market-maker poolMarket Maker Service
ServicegRPC PackagePurpose
Risk Servicerisk.v1PreTradeCheck — leverage / margin / exposure validation
Wallet Servicewallet.v1LockFunds, UnlockFunds, SettleTrade — margin lifecycle
Metadata Servicemetadata.v1Instrument lookup — tick size, lot size, min notional, fee schedule
Market Data Servicemarketdata.v1Last-traded price for sizing market orders

TLS is enabled automatically for non-localhost connections.

{
"type": "record",
"name": "OrderCommand",
"fields": [
{ "name": "command_type", "type": "string" },
{ "name": "order_id", "type": "string" },
{ "name": "account_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" }
]
}
  1. Auth: Extract x-user-id, x-account-id, x-source from headers (set by API Gateway after JWT validation)
  2. Idempotency Check: Check idempotencyKey in Redis (24h TTL) → if hit, return cached response
  3. DTO Validation: NestJS ValidationPipe runs class-validator decorators on CreateOrderDto
  4. Instrument Lookup: Fetch instrument from Metadata Service (gRPC, cached in Redis 5 min)
  5. Business Validation: Check quantity tick/lot size, min notional, price precision, leverage limits
  6. Market Price Lookup (market orders only): MarketDataService.GetMarkPrice for sizing
  7. Risk Check: RiskService.PreTradeCheck via gRPC (leverage, margin, exposure)
  8. Wallet Lock: WalletService.LockFunds via gRPC (reserve margin)
  9. Persist: Insert orders row (status = new) + order_events row + outbox row in a single transaction
  10. Publish: Outbox relay polls and publishes order.command.v1 to Kafka
  11. Response: Return HTTP 202 with the order in new status

When engine.event.v1 arrives:

  1. Deduplicate by engine sequence number (Redis)
  2. Parse event (ORDER_ACCEPTED, TRADE, ORDER_CANCELED, ORDER_REJECTED, BOOK_DELTA — last one ignored)
  3. Load order from DB
  4. Apply state transition via the state-machine service
  5. For fills: call WalletService.SettleTrade() to apply realized P&L and fees
  6. Insert order_events row (audit trail)
  7. Update order status, filled_qty, avg_fill_price
  8. 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.

ScenarioHandling
Risk service unavailableReject order with RISK_CHECK_UNAVAILABLE
Wallet lock failsReject order with INSUFFICIENT_BALANCE
Kafka publish failsOutbox pattern retries; order stays in new until published
Engine event processing failsConsumer retries with backoff; idempotent by trade_id
DB transaction failsAutomatic rollback; client retries with same idempotency key
MetricTarget
Order acceptance latency (p95)< 100 ms
Outbox drain latency (p95)< 500 ms
Fill processing latency (p95)< 200 ms
Order creation success rate>= 99.5%
VariableDescriptionDefault
POSTGRES_URLPostgreSQL connection stringRequired
REDIS_URLRedis connection stringRequired
KAFKA_BROKERSKafka broker addressesRequired
RISK_SERVICE_GRPC_URLRisk service gRPC endpointRequired
WALLET_SERVICE_URLWallet service gRPC endpointRequired
METADATA_SERVICE_URLMetadata service gRPC endpointRequired
MARKETDATA_SERVICE_GRPC_URLMarket Data service gRPC endpointRequired
ORDER_GRPC_PORTInbound gRPC server (OrderQueryService)50060
RATE_LIMIT_PER_ACCOUNTMax requests per second per account50
  • Language: TypeScript
  • Framework: NestJS (Fastify adapter)
  • ORM: Drizzle ORM
  • Database: PostgreSQL
  • Cache: Redis (idempotency, rate limiting, instrument cache, engine-seq dedup, list cache)
  • Messaging: Confluent Kafka with Avro serialization via Schema Registry
  • gRPC: @nestjs/microservices with gRPC transport
  • Validation: class-validator + class-transformer via NestJS ValidationPipe
  • OpenAPI: @nestjs/swagger (UI at /docs)
  • Decimal arithmetic: fraction.js
  • Package Manager: Bun