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.
  • 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)]
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
MethodEndpointDescription
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
MethodEndpointDescription
GET/v1/tradesList all trades
GET/v1/trades/:instrumentList trades for instrument

Create Order Request:

{
"instrumentId": "uuid",
"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"
}

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)
{
"type": "record",
"name": "OrderCommand",
"fields": [
{ "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" }
]
}
  1. Auth: Extract x-user-id, x-account-id from headers (set by API Gateway after JWT validation)
  2. Idempotency Check: Check idempotency_key in Redis → if hit, return cached response
  3. Schema Validation: Validate request body with Zod schemas
  4. Instrument Lookup: Fetch instrument from Metadata Service (gRPC, cached in Redis)
  5. Business Validation: Check quantity tick/lot size, min notional, price precision, leverage limits
  6. Risk Check: Call RiskService.PreTradeCheck() via gRPC (leverage, margin, exposure)
  7. Wallet Lock: Call WalletService.LockFunds() via gRPC (reserve margin)
  8. Persist: Insert orders row (status = new) + order_events row + outbox row in single transaction
  9. Publish: Outbox relay publishes order.command.v1 to Kafka
  10. Response: Return order with status new

When engine.event.v1 arrives:

  1. Parse event (fill, cancel, reject)
  2. Load order from DB
  3. Apply state transition
  4. For fills: call WalletService.SettleTrade() to apply P&L and fees
  5. Insert order_events row
  6. Update order status, filled_qty, avg_fill_price
  7. 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_GRPC_URLWallet service gRPC endpointRequired
METADATA_SERVICE_GRPC_URLMetadata service gRPC endpointRequired
RATE_LIMIT_PER_ACCOUNTMax requests per second per account50
  • 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