- What outcome are we optimizing for? β Order execution quality: fill rate (% of orders that execute), price improvement (did the customer get a better price than the quoted spread?), and execution speed. Secondary: platform reliability during volatile markets (the system MUST NOT go down during a meme-stock spike β that's when customers need it most and when Robinhood has historically failed). This shapes priority ordering: CORRECTNESS > AVAILABILITY > SPEED.
- Core products? β Stocks, ETFs, options, and crypto trading. Commission-free. Revenue from payment for order flow (PFOF) and interest on uninvested cash.
- Order types? β Market orders (execute immediately at best price), limit orders (execute only at specified price or better), stop orders, stop-limit orders.
- Who executes trades? β NOT us. We route orders to external market makers via FIX protocol. They execute and report back fills.
- Settlement? β T+1 (trade date + 1 business day). Shares and cash settle through a clearinghouse. Robinhood self-clears via Robinhood Securities.
- Scale? β ~25M funded accounts, ~750K requests/sec normal, ~5M req/sec during meme-stock events. ~5-10M orders/day.
- Regulatory? β SEC, FINRA regulated. Every order, fill, cancellation must be auditable. Pattern day trading rules, margin requirements, trading halts must be enforced.
| In Scope | Out of Scope |
|---|---|
| Order placement & lifecycle (state machine) | Options pricing / Greeks computation |
| Order routing to market makers | Crypto wallet / blockchain internals |
| Real-time market data streaming | Tax lot optimization details |
| Portfolio & position tracking | Margin lending risk models |
| Double-entry ledger (cash & positions) | Customer support workflows |
| Risk checks (buying power, trading halts) | Regulatory reporting file formats |
| Sharding architecture for blast radius | Mobile app frontend |
- UC1 (View Market Data): User opens app β sees real-time prices for watched stocks, updated every second via WebSocket. ~25M concurrent connections during market hours.
- UC2 (Place Order): User taps "Buy 10 shares of AAPL at market" β system validates buying power, reserves funds, routes to market maker, receives fill, updates positions. End-to-end: <500ms.
- UC3 (Partial Fill): Limit order for 1,000 shares β market maker fills 400, then 300, then 300 across 3 separate fill events. System must correctly update position after each partial fill.
- UC4 (View Portfolio): User sees: cash balance, buying power, total equity, positions with current market value, P&L per position, order history. Values must be EXACT β not eventually consistent.
- UC5 (Meme Stock Spike): GameStop event. Traffic jumps 7Γ in minutes. System must not drop orders, must enforce risk controls, may restrict trading if clearing deposit requirements spike.
- Financial correctness above all: Every cent must be accounted for. No phantom shares. No double-counted cash. The ledger is the source of truth, and it must balance at all times.
- ACID for trades: Order state transitions, balance updates, and position changes must be transactional. Partial updates are a regulatory violation.
- Eventually consistent is OK for: Market data quotes (advisory, not authoritative). Notifications. Leaderboards.
- Availability during market hours: 9:30 AM β 4:00 PM ET. Pre/after-hours: 7 AM β 8 PM ET. Downtime during market hours is catastrophic.
- Auditability: Every order, fill, cancellation, balance change is logged with timestamps. Regulators can request full reconstruction of any account's history.
| Requirement | Decision | Why (and what was rejected) | Consistency |
|---|---|---|---|
| Financial correctness: no phantom shares | CockroachDB with serializable isolation | Order state transitions + balance updates must be atomic. No eventual consistency for money. CockroachDB = distributed SQL with ACID. | CP |
| Need active-active for market hours DR | CockroachDB (not PostgreSQL) | Raft consensus across regions. If US-East fails during trading, US-West continues. PostgreSQL requires manual failover. | CP |
| Ledger must always balance | Double-entry bookkeeping in single transaction | Stored procedure inserts debit + credit atomically. CHECK constraint verifies sum = 0. Single-row updates could lose money. | CP |
| Live market data to 25M connected users | Kafka β WebSocket fan-out (not polling) | Price updates pushed instantly. Polling at 1s intervals = 25M requests/sec wasted. Fan-out by symbol allows independent scaling. | β |
| Blast radius isolation for incidents | Shard by user_id (not by symbol) | A bug affecting shard 7 impacts 1/N users, not all users trading AAPL. Symbol-based sharding would make hot stocks a single point of failure. | β |
| Regulatory: 7-year audit trail | Append-only Kafka log + immutable archive | Order events retained in Kafka (30 days) then archived to S3 (7 years). FINRA/SEC can examine any historical order. | β |
π± API Gateway INGRESS
- Auth (OAuth2 + MFA), rate limiting
- Routes: market data β read path, orders β write path
- WebSocket upgrade for real-time data
π Market Data Service READ PATH
- Ingests real-time quotes from exchanges/market makers
- Publishes via Kafka β WebSocket fan-out
- Best-effort delivery. Stale quotes are OK.
π Order Service WRITE PATH
- Accepts orders, validates, persists state machine
- Pre-trade risk checks (buying power, halts)
- Routes to market makers via FIX protocol
π Shard Router ROUTING
- Maps user_id β shard_id
- Each shard: own app servers + own PostgreSQL
- Blast radius: shard failure affects only that shard's users
π° Ledger Service FINANCE
- Double-entry accounting for every money movement
- Source of truth for balances and positions
- Immutable append-only journal
β‘ Execution Gateway EXTERNAL
- FIX protocol connection to market makers
- Sends orders out, receives fill reports
- Handles partial fills, rejects, cancellations
π‘οΈ Risk Engine GUARD
- Pre-trade: buying power, margin, pattern day trader
- Real-time: position limits, concentration risk
- Market-level: halt enforcement, circuit breakers
π¦ Settlement / Clearing BACK OFFICE
- T+1 settlement with DTCC / clearinghouse
- Reconciliation: match internal records with street-side
- Deposit/withdrawal (ACH transfers)
Idempotency
- Client-side: Every order request includes a unique
idempotency_key(UUID generated by the client). If the same key is sent twice (e.g., network retry), the server returns the same response without creating a duplicate order. - Market-maker side: Each order sent to the market maker has a unique
clOrdID(FIX protocol). Fill reports reference this ID. Duplicate fill reports (network retry from market maker) are deduplicated by checking if this fill was already processed. - Ledger side: Every ledger entry has a unique
transaction_idderived from the order event. Re-processing the same event produces the same transaction_id β no double-counting.
Buying Power Calculation
- Append-only: Ledger entries are NEVER updated or deleted. A correction is a new entry that reverses the original (debit becomes credit and vice versa). This ensures full auditability β regulators can reconstruct any account's history from the ledger alone.
- Balance computation: Current balance =
SUM(credits) - SUM(debits)for an account. For performance, we maintain a materializedaccount_balancestable that's updated in the SAME transaction as new ledger entries. The raw ledger is the source of truth; the balance table is a cached aggregate. - Reconciliation: End-of-day process computes balances from raw ledger entries and compares to the balance table. Any discrepancy is a critical alert. Separately, internal balances are reconciled against the clearinghouse's records (street-side reconciliation).
UPDATE balance = balance - 2095 is fragile: if the process crashes after debiting cash but before crediting shares, money disappears. Double-entry in a single transaction ensures atomicity: BOTH sides of the entry are written together or neither is. The invariant (debits = credits) is a built-in consistency check β if it EVER doesn't balance, you know there's a bug. Simple balance fields have no such self-checking property.- Shard count: Started with ~10 shards, grown to ~50+. Each shard serves ~500K users. Can add shards indefinitely β just assign new users to new shards.
- Shard migration: Moving a user between shards requires: (1) halt trading for that user briefly, (2) copy all data (orders, ledger, positions) to new shard, (3) update shard_mapping, (4) resume. Rare operation β only for rebalancing.
- Cross-shard queries: The aggregation layer handles these, but they're slow (fan-out + merge). The architecture avoids cross-shard queries on the hot path. Portfolio view = one shard only (all user data colocated).
| Data | Store | Why This Store |
|---|---|---|
| Orders & positions | CockroachDB (sharded) | Serializable isolation for order state transitions. Sharded by user_id. Geo-distributed for DR. ACID required β no eventual consistency for money. |
| Ledger (double-entry) | CockroachDB | Every transaction = debit + credit entry. Append-only. Sum of all entries must equal zero. Audit trail is immutable. |
| Market data (live) | Redis | Latest quotes per symbol. Updated by market data ingest service. TTL-based expiration. Read by WebSocket fan-out. |
| Market data (historical) | PostgreSQL / TimescaleDB | OHLCV candles for charts. Time-series partitioned. Queried for sparklines and full charts. |
| Order events | Kafka | order.placed, order.filled, order.cancelled. Consumed by ledger, notifications, compliance, analytics. |
| User accounts & KYC | PostgreSQL | PII, SSN (encrypted at rest), KYC status, account type. Relational integrity with orders. |
- Trade date (T): Order is filled. User sees shares in portfolio and reduced cash. Internally, the ledger shows shares as "pending settlement."
- T+1 (next business day): Actual settlement. DTCC (clearinghouse) transfers shares from seller's broker to buyer's broker. Cash moves in the opposite direction. Robinhood's clearing arm reconciles with DTCC.
- Deposit requirement: Between T and T+1, Robinhood must deposit collateral with DTCC proportional to unsettled trades. During the GME event, DTCC increased this requirement dramatically β Robinhood had to restrict buying to manage deposit requirements. This is a LIQUIDITY RISK, not a technology failure.
- Netting: At end of day, buys and sells in the same stock are netted. If 1M shares of AAPL were bought and 900K sold across all users, only the NET 100K shares actually settle. Reduces capital requirements.
- Audit trail: Every order, fill, cancellation, account change stored permanently. Immutable append-only log. SEC/FINRA can request records for any time period.
- Pattern Day Trader (PDT): If account has <$25K and executes 4+ day trades in 5 business days β account flagged, trading restricted. Enforced by the risk engine before every order.
- Trading halts: Exchange can halt trading on a symbol (circuit breaker). Risk engine subscribes to halt feeds and blocks orders on halted symbols. Must be enforced within milliseconds of the halt announcement.
- Best execution: SEC Rule 606 requires brokers to demonstrate they route orders to get the best price for users. Robinhood must track and report execution quality metrics by market maker.
- KYC/AML: Know Your Customer / Anti-Money Laundering checks at account opening. Suspicious activity monitoring on transactions (large deposits, rapid trading patterns).
- Order pipeline metrics: Orders per second, fill latency (time from ROUTED to FILLED), rejection rate, stuck order count (ROUTED for >30s).
- Ledger health: Balance reconciliation status, transaction rate, any imbalance alerts.
- Per-shard health: DB connection pool usage, query latency p99, replication lag. Dashboard showing all 50 shards at a glance.
- Market data pipeline: Quote staleness (time since last update per symbol), WebSocket connection count, fan-out latency.
| Extension | Architecture Impact |
|---|---|
| Options Trading | Far more complex order types (spreads, straddles). Greeks computation requires real-time pricing models. Position tracking becomes multi-dimensional (strike, expiry, underlying). Risk engine complexity increases dramatically. |
| Crypto Trading (24/7) | Markets never close β no "overnight" for maintenance. Settlement is blockchain-based (not T+1). Wallet management adds custody complexity. Price feeds from decentralized sources with varying reliability. |
| Fractional Shares | User buys 0.37 shares of AAPL. Exchange only trades whole shares β Robinhood must aggregate fractional orders into whole-share orders, execute, and allocate fills proportionally. A "mini-exchange" within the broker. |
| Real-Time P&L Streaming | Portfolio value changes every time any held stock's price changes. With 25M users Γ avg 5 positions Γ ~100K price updates/sec, this is a massive computation: recalculate and push P&L for every affected user. Requires a streaming computation engine (Flink/Kafka Streams). |
| Smart Order Routing | Route to the market maker offering the best price improvement, not just the default. Requires real-time comparison of execution quality across market makers. Regulatory pressure to demonstrate best execution. |
Why CockroachDB instead of PostgreSQL for the order service?
Two reasons: geographic distribution and serializable isolation at scale. Robinhood needs active-active in at least two regions for disaster recovery β if US-East goes down during market hours, orders must still process. CockroachDB provides this natively with consensus-based replication across regions. PostgreSQL would require complex leader-follower replication with manual failover. Second, order state transitions need serializable isolation β when we go from PENDING β FILLED and update the user's cash balance, no other transaction should see an intermediate state. CockroachDB provides serializable by default without the performance cliff that PostgreSQL experiences under serializable isolation. The tradeoff: CockroachDB has higher write latency than single-node PostgreSQL (~10-20ms vs ~2ms) due to consensus rounds, but that's acceptable for order processing where correctness matters more than speed.
How do you ensure the ledger always balances?
By making it physically impossible to create an unbalanced entry. Every ledger write is a single database transaction that inserts exactly two rows: a debit and a credit of equal amounts. The application code doesn't INSERT individual rows β it calls a stored procedure that takes (from_account, to_account, amount) and atomically creates both entries. The procedure has a CHECK constraint: it queries the running sum of all entries, and if the result isn't zero, the transaction aborts. Additionally, a nightly batch job independently recalculates all account balances by summing all ledger entries per account and compares against the cached balance. Any discrepancy triggers a P1 alert. This belt-and-suspenders approach means: the application layer CAN'T create imbalances (procedural enforcement), and even if a bug somehow did, the nightly reconciliation catches it within hours.
What happens if a market maker fills an order but the acknowledgment is lost?
This is the FIX protocol's bread and butter. Every order has a unique ClOrdID (client order ID) that's generated by Robinhood's Execution Gateway. The market maker returns an ExecutionReport referencing this ClOrdID. If the acknowledgment is lost, the Execution Gateway resends a StatusRequest with the same ClOrdID. The market maker responds with the current status of that order β either filled, rejected, or pending. Crucially, the order's state in our system doesn't change until we receive a confirmed ExecutionReport. If we sent the order and got no response, the order stays in SENT state. A reconciliation process runs every minute: for any order in SENT state for >30 seconds, it queries the market maker. If the query also fails, we escalate to the manual operations desk. We NEVER assume an order was filled without confirmation β assuming a fill and updating the user's portfolio with phantom shares would be a regulatory violation.
How do you handle the GameStop scenario β extreme load on a single stock?
The system is sharded by user_id, not by symbol, so a single hot stock doesn't create a database hotspot. However, it creates three other hotspots: (1) Market data: AAPL price updates need to reach everyone watching it. We pre-partition the WebSocket fan-out by symbol, so we can scale the GME partition independently β add more fan-out servers for that specific symbol. (2) Order volume: 50x normal order rate for one stock. The Shard Router distributes these across all user shards (since different users are on different shards), so no single shard is overwhelmed. (3) Execution Gateway: all GME orders route to the same market makers. We maintain multiple FIX sessions per market maker and load-balance across them. The circuit breaker pattern applies: if a market maker stops responding within SLA, we fail fast and try alternate venues. The controversial aspect: Robinhood can also restrict trading on specific symbols as a business decision (with proper disclosure), which is what happened with GME.