0) Problem Restatement
Uber asked: design a system where users create price alerts on securities: "Notify me when AAPL goes above $200", "when TSLA falls below $150", or "when a stock moves 5% today". The system receives a real-time price feed and must notify users when the price crosses their threshold, quickly and without spamming.
1) Requirements
- Create, list and delete alerts (symbol, direction above/below, threshold, one-time or recurring).
- Evaluate against a live price feed (thousands of symbols, many updates per second).
- Notify via push, email or SMS within seconds.
- Don't send duplicates. One-time alerts fire once, and recurring alerts re-arm after the price moves back.
1.1 Scale
- 10M users, 50M active alerts. Price updates: ~10K symbols × several per second.
2) Architecture
Architecture Diagram
flowchart LR
U["Users"] --> API["Alert API"]
API --> DB[("Alerts DB")]
DB -->|"load + changes"| M["Matchers - partitioned by symbol"]
FEED["Market data feed"] --> K[("Kafka - ticks by symbol")]
K --> M
M --> T[("Triggered alerts")]
T --> N["Notification service - dedupe, rate limit"]
N --> U3) Efficient Matching (the core)
Checking all 50M alerts on every tick is impossible. Instead, per symbol, keep alerts in two sorted structures in memory:
- Above-alerts sorted by threshold ascending.
- Below-alerts sorted by threshold descending.
When a new price p arrives for AAPL (previous price p0):
- Up move: all above-alerts with
p0 < threshold ≤ pjust got crossed. Find them with a binary search and take that range. The cost is O(log n + k), where k = alerts triggered. - Down move: similarly, the below-alerts with
p ≤ threshold < p0. - One-time alerts that fire are removed from the structure. Recurring ones are marked "fired" and re-armed only after the price moves back past the threshold by a small margin (hysteresis, to avoid spam when the price wiggles around the threshold).
Matchers are partitioned by symbol (e.g., via Kafka partitions), so each machine holds the alerts for its symbols in memory (50M alerts × ~40 bytes ≈ 2 GB, spread across machines).
4) Keeping Matchers in Sync
- On startup, a matcher loads the alerts for its symbols from the DB.
- New, updated or deleted alerts are published as events (CDC), and the matcher updates its in-memory structures.
- If a matcher crashes, another one takes over its partitions and reloads them from the DB.
5) Notifications
- Triggered alerts go to a queue. The notification service sends push, email or SMS, with an idempotency key
(alert_id, trigger_time)so retries don't double-send. - Rate limits per user (e.g., at most 20 alerts per hour), and batching in volatile markets ("5 of your alerts triggered").
- Percent-change alerts: compare against the previous close (stored per symbol), and index them by their equivalent price threshold for today.
6) Wrap-Up
Store alerts in a DB, and load them into in-memory per-symbol sorted threshold lists on matcher nodes partitioned by symbol, kept current via change events. On each price tick, binary-search the range of thresholds crossed between the previous and new price (O(log n + k)), remove one-time alerts and re-arm recurring ones with hysteresis, and send notifications through an idempotent, rate-limited, batching notification service.