At a glance#
| Number | Value | What it drives |
|---|---|---|
| Orders a day (design) | 15M busy · 40M spike | ~670/s average over a 22,500 s session |
| First minute at 9:15 | 7,500/s · peak second ~15,000/s | 11× average; 40,000/s on a spike day (assumed 3 % share, 2× second) |
| Edge capacity | 50,000 order requests / s | 1.25× spike-day peak |
| Reads at the open | ~167,000 / s | own read path from memory, apart from orders |
| Market data | 1.5M sockets · ~30M packets / s | binary ~12 Gbps vs JSON ~60 Gbps; ~60 ticker servers |
| RMS state | 7M accounts × 2 KB ≈ 14 GB | 16 in-memory partitions, ~2,500 checks/s each at peak |
| Exchange sessions | ~90 at 60 % load | 18,000 in flight cap (90 × 200 unacked) |
| Audit | ~10 TB / yr · ~80 TB / 8 yr | append-only order events, kept 8 years |
Architecture#
DiagramKite on one page: three paths behind one edge
%% title: Kite on one page: three paths behind one edge
flowchart TD
APP@{ icon: "sd:phone", label: "Trading apps · web, Android, iOS<br/>API clients · algos" } ==> GW["API gateway<br/>auth · client bucket 10/s · admission"]:::edge
GW -->|"reads"| HOT@{ icon: "tech:redis", label: "Hot Redis cache<br/>per-user orders, positions, margins" }
GW ==>|"place, modify, cancel"| OMS["Order service<br/>state machine · one order per key"]:::core
OMS ==>|"check + block"| RMS["RMS · 16 in-memory partitions<br/>one thread per partition · journalled"]:::hot
OMS ==> ADM@{ icon: "sd:queue", label: "Admission queue<br/>cancel · exit · modify · new lanes<br/>deadlines · 18,000 in-flight cap" }
OMS --> BUS@{ icon: "tech:kafka", label: "Kafka<br/>order, fill, funds events" }
BUS --> ODB@{ icon: "sd:log", label: "Order + audit store<br/>append-only · 8 years" }
BUS -.->|"projections"| HOT
RMS -.-|"posted funds"| LED@{ icon: "sd:database", label: "Funds ledger<br/>double entry · pay-ins, payouts" }
ADM ==> EGW["Exchange gateways<br/>~90 sessions · leased lines · 2 DCs<br/>mark SENT, then send"]:::service
EGW ==> EX@{ icon: "sd:building", label: "NSE · BSE · MCX<br/>match, ack, trade" }
EX --> FH["Feed handlers<br/>~100,000 updates/s at open"]:::service
FH --> TK["Ticker servers · ~60<br/>latest snapshot · binary · 1 flush/s"]:::core
TK --> WS@{ icon: "tech:websocket", label: "WebSocket edge · 1.5M sockets<br/>binary ticks + sequenced order updates" }
class APP client
class HOT cache
class ADM,BUS queue
class ODB,LED store
class EX external
class WS edge- Orders take the exact path: one row per (client, key), margin blocked in one step, SENT recorded before the bytes leave. Prices are lossy snapshots. Reads come from per-user views and may be a moment stale.
- The order service is the only writer of order state; order book, positions and P&L are projections of its events.
- The exchange owns matching and is the truth for order state; the broker reconciles to it.
DiagramRMS partition: check and block in one step
%% title: RMS partition: check and block in one step
flowchart TD
ORD(["Order from the order service"]):::client --> ROUTE["Hash client id → 1 of 16 partitions<br/>available = cash + collateral − blocked"]:::service
ROUTE --> CHK{"required ≤ available?"}:::core
CHK -->|"yes"| BLK["Block margin<br/>blocked += required"]:::hot
CHK -->|"no"| REJ(["Reject with both numbers<br/>needs ₹70,000 · has ₹30,000"]):::bad
BLK --> JRN@{ icon: "sd:log", label: "Journal<br/>fsync batched ~1 ms, before answering" }
JRN --> OK(["Validated · on to admission"]):::ok
LED@{ icon: "sd:database", label: "Funds ledger<br/>source of truth for money" } -.->|"posted pay-in event"| ROUTE
JRN -.-> SB["Standby follows journal<br/>takes over in seconds"]:::service
class JRN,LED storeKey flows#
DiagramOne order at 9:15:00, tap to fill
%% title: One order at 9:15:00, tap to fill sequenceDiagram autonumber participant A as Asha's app participant G as Gateway participant O as Order service participant R as RMS participant X as Exchange gateway participant E as Exchange A->>G: Buy 50, limit · key k7 G->>O: admitted, bucket has a token O->>O: insert once per (client, k7) · RECEIVED O->>R: check and block margin R-->>O: OK, blocked and journalled O-->>A: order 8812 · VALIDATED (~30 ms) O-)X: order.send via outbox X->>X: mark SENT, then write bytes X->>E: new order · broker ref 8812 E-->>X: accepted · exchange order id X-)A: OPEN, pushed over WebSocket E-->>X: trade · 50 at limit X-)A: COMPLETE · position updated Note over A,E: a retry with key k7 just reads order 8812 back
DiagramLine drops before the ack: UNKNOWN, then reconcile
%% title: Line drops before the ack: UNKNOWN, then reconcile
sequenceDiagram
autonumber
participant O as Order service
participant X as Gateway DC-A
participant E as Exchange
participant R as Reconciler
participant A as App
O->>X: send order 8812
X->>E: new order · ref 8812
Note over X,E: line drops before the ack
X-->>O: no answer in 2 s · UNKNOWN
O-)A: Sent, confirming. Don't place it again
Note over O: never resend · never say failed
R->>E: status of ref 8812 via DC-B
alt found at the exchange
E-->>R: open · exchange order id
R->>O: UNKNOWN → OPEN
else not found after full resync
E-->>R: no such order
R->>O: UNKNOWN → NOT_PLACED · release margin
end
O-)A: final state, in plain wordsDiagramTwo orders, one rupee: serialised on the partition
%% title: Two orders, one rupee: serialised on the partition
sequenceDiagram
autonumber
participant D1 as Device 1 via OMS-a
participant D2 as Device 2 via OMS-b
participant P as RMS partition 7
Note over P: available ₹1,00,000 · one thread
D1->>P: block ₹70,000 (order 1)
D2->>P: block ₹70,000 (order 2)
P-->>D1: OK · available now ₹30,000
alt ₹50,000 pay-in already posted by the ledger
P-->>D2: OK · both pass
else pay-in not posted yet
P-->>D2: reject · needs ₹70,000, has ₹30,000
Note over D2: client retries a moment later
endData model#
DiagramCore entities
%% title: Core entities
classDiagram
class Order {
+orderId : string
+clientId : string · partition key
+idemKey : string · unique with clientId
+qty : int
+filled : int
+state : OrderState
+exchSeq : int · last exchange msg applied
}
class OrderEvent {
+orderId : string
+state : string
+reason : string
+at : timestamp
}
class Account {
+clientId : string
+cash : paise
+collateral : paise · after haircut
+blocked : paise
+ledgerSeq : int · last posting applied
}
class MarginHold {
+orderId : string
+amount : paise
}
class LedgerPosting {
+seq : int
+clientId : string
+amount : paise
+kind : pay-in / payout / settlement
}
Order "1" --> "many" OrderEvent : audited as
Account "1" --> "many" MarginHold : blocks
MarginHold "1" --> "1" Order : for
Account "1" --> "many" LedgerPosting : funded by
Account "1" --> "many" Order : places- RMS state partitions by hash of client id (16 partitions, ~0.9 GB each); orders key on (client, idempotency key).
- ~5 events × ~500 B per order: 37.5 GB on a normal day, 100 GB on a spike day. Ticks: 44 B quote packet + 2 B length, per subscribed instrument.
Lifecycle#
DiagramOrder state machine
%% title: Order state machine stateDiagram-v2 [*] --> RECEIVED: inserted once per key RECEIVED --> VALIDATED: margin blocked RECEIVED --> REJECTED: RMS says no VALIDATED --> SENT: marked, then sent VALIDATED --> REJECTED: past queue deadline SENT --> OPEN: exchange accepts SENT --> REJECTED: exchange rejects SENT --> UNKNOWN: no answer in time UNKNOWN --> OPEN: reconciled, found UNKNOWN --> NOT_PLACED: reconciled, not found OPEN --> COMPLETE: filled OPEN --> CANCEL_PENDING: cancel sent OPEN --> CANCELLED: exchange cancels rest CANCEL_PENDING --> CANCELLED: cancel confirmed CANCEL_PENDING --> COMPLETE: fill won the race COMPLETE --> [*] REJECTED --> [*] CANCELLED --> [*] NOT_PLACED --> [*] class COMPLETE ok class REJECTED,CANCELLED,NOT_PLACED bad class UNKNOWN queue
Decisions#
| Decision | Chose | Over | Because |
|---|---|---|---|
| Duplicate taps | Key per tap + unique (client, key) | Dedupe in the app | a retry on any server reads the same row |
| Send ordering | Mark SENT, then send | Send, then record | worst case is UNKNOWN, never a lost send |
| No ack | UNKNOWN + reconcile | Resend or mark failed | it may already be at the exchange |
| Margin check | In-memory partition, one thread each | DB row lock per order | 40,000 checks/s, no double spend |
| Market data | Binary snapshots, coalesced, 1 flush/s | JSON per exchange update | ~12 Gbps vs ~60; old prices are worthless |
| Slow sockets | Skip flush, close after ~10 s | Buffer everything | one phone can't grow server memory |
| Open-bell capacity | Pre-warm on a schedule by 8:45 | Autoscaling | the spike is over before an autoscaler reacts |
| Overload | Priority lanes + queue deadlines | One FIFO queue | cancels and exits first; no stale-price orders |
When it breaks#
| Failure | What users see | Contain it with |
|---|---|---|
| Leased line flaps | orders confirm on the other DC | judge sessions by success + ack latency; reconcile in-flight, never move them |
| Exchange slows to ~800 ms | some "not sent, system busy" | Little's law ceiling 22,500/s; deadlines; idempotent retries |
| Exchange feed freezes (24 Feb 2021) | "Prices delayed since 10:08" | detect staleness, not just disconnect; market protection |
| RMS partition crashes at 9:15 | that partition's clients wait seconds | standby follows the journal |
| Refresh storm at the open | reads slow, orders fine | read path from memory; degrade charts, depth, reports first |
| Ticker server restarts | brief price gap | jittered reconnect; snapshot on next flush |
Say it in the interview#
- "A timeout means I don't know." The only safe move is to ask the exchange, not to send again; that is the UNKNOWN state.
- Split traffic by what it can lose: orders lose nothing, prices keep only the latest, reads can be a moment stale.
- One key per tap makes the double tap and every retry a cheap read of the same order.
- One writer per account: hash clients to RMS partitions so check-and-block has nothing in between.
- The tower is on the calendar: pre-warm by 8:45, validate after-market orders before 9:15, never rely on autoscaling.
- Little's law sets the ceiling: 18,000 in flight ÷ 0.8 s = 22,500/s when the exchange slows; protect cancels and exits.
- Tell the truth on screen: "Sent, confirming. Don't place it again" stops one order becoming three.