System design sample solution

System design: price alerts

A step-by-step system design answer you could actually give in 60 minutes.

System design interview · Sample 60-minute answer · Not Bloomberg's actual design · Independent, not affiliated with Bloomberg

The system design question

Users set price rules, like "tell me when this stock goes above a level" or "tell me if it moves 5% in 10 minutes". Design the system that checks every price tick against those rules and notifies them.

Agree on the scope (5 minutes)

Say up front what you're covering.

We'll designWe'll skip
Creating and deleting rulesCollecting and cleaning exchange feeds
Level rules: above or below a priceStoring years of price history
Move rules: up or down X% in Y minutesCharts and the trading UI
Checking every tick against the rulesThe push and email providers themselves
Sending the notification

Then agree on what "good" means:

  • Fast: a notification should reach the user within a second or two of the tick that set it off.
  • Once: a rule fires one time when it's hit, not again on every tick after.
  • Small memory: a 10 minute rule can't mean keeping 10 minutes of every tick for every stock.
  • Keeps up: the busiest minute of the trading day can't make us fall behind.

Rough numbers (5 minutes)

Made up, but reasonable: about 100,000 tickers, around 500,000 price ticks a second on average during market hours and 5 times that at the open and close. 10 million users with 50 million rules between them, and about 1 in 10 is a move rule. Move rules pick from a short menu of windows, up to 60 minutes.

~500K
ticks per second, on average
~2.5M
ticks per second at a peak
50M
active rules
~5M
of them move rules
QuestionRough mathWhat it means
Can the rules fit in memory?50M rules × about 100 bytesAbout 5 GB. Fits in memory, split across a few machines
What does a rolling window cost?100K tickers × 3,600 one-second slots × 16 bytes for a low and a highAbout 6 GB for a full hour on every ticker. Fine when split, and only tickers with move rules need it
How many rules fire?Say 1% of rules trigger on a busy day500K notifications a day, but they bunch up when the market jumps

The overall design (10 minutes)

Price streamalready cleanedSplit by tickersame key everywhereRule matchersrules and windows in memoryFired eventsdeduped by rule IDNotifierpush, email, in-app
Ticks flow by ticker to matchers that hold that ticker's rules and price windows in memory. Fired rules go on a queue to the notifier.
  1. We assume a clean price stream already exists: one format, one tick per price change, with a ticker, price and timestamp. That's its own design problem, so say so and move on.
  2. The stream is split by ticker, so every tick for one ticker lands on the same matcher, in order.
  3. Each matcher owns a slice of tickers. For each one it keeps the level rules sorted by price, the move rules grouped by window, and a small rolling summary of recent prices.
  4. When a rule fires, the matcher writes a 'rule fired' event to a queue. A notifier reads it and sends the push, email or in-app message.
  5. Rules are created through a normal API that writes to a database first, then tells the right matcher.

What the apps need to do

ActionWho does itWhat's sent
Create a ruleUserTicker, the rule (above or below a price, or up or down X% in Y minutes), one-time or repeating, and how to notify
List or delete rulesUserRule IDs
Rule changed eventRule API, to the matcherRule ID, ticker, the rule, and whether it was added or removed
Rule fired eventMatcher, to the notifierRule ID, user ID, the tick that set it off, and the move it saw

What we store and where

DataWhere it goesWhy
RulesRelational database, split by userThe source of truth for what users set up
Level rules by tickerIn each matcher's memory, sorted by priceSo a tick only looks at rules near its price
Move rules by ticker and windowIn each matcher's memory, sorted by percentSo one calculation per window covers every rule on it
Rolling price summaryIn each matcher's memory, a low and high per secondAnswers 'how far has it moved' without keeping every tick
TicksThe stream, kept a few daysSo a matcher can replay and rebuild its windows after a crash
Fired rulesDatabase with a unique key on rule ID and firing numberSo the same firing can't be sent twice

Step 1: Checking level rules quickly (10 minutes)

The naive way checks every rule on every tick. That's 2.5 million ticks times 50 million rules, which is never going to happen. Instead group rules by ticker, and within a ticker, sort them by price.

For one ticker, keep two sorted lists: 'above X' rules and 'below X' rules. When the price goes from 101 to 105, only the 'above' rules between 101 and 105 fire. Find that range with a quick binary search, fire those, and take them out. Most ticks fire nothing and cost almost nothing to check.

Split tickers across matchers using the same key as the price stream, which is sharding by ticker. A few tickers are much busier than the rest, so give those their own matchers instead of hashing them in with everyone else.

New rules go to the database first, then to the right matcher. When one arrives, check it against the current price right away, in case the price is already past it.

Step 2: Move rules without keeping the whole stream (12 minutes)

'Up 5% in 10 minutes' means: is the price now at least 5% above the lowest price in the last 10 minutes? 'Down 5%' means at least 5% below the highest. So for each window, all you need is the low and the high over that window. You don't need every tick.

Keep one slot per second for each ticker, holding that second's low and high, in a ring that's as long as the longest window on the menu, say an hour. Each tick only updates the current slot. When a second passes, the oldest slot drops off. That's a window over the stream, and its size stays fixed no matter how many ticks come in.

To get the low over the last 10 minutes, you could scan 600 slots, but doing that on every tick for busy stocks adds up. Instead keep a running low and high per window length, using the trick of keeping only slots that could still be the answer. It's a short list that's cheap to update. Since windows come from a fixed menu, that's at most a handful per ticker.

Now group the move rules by ticker and window, and sort them by percent. On a tick, work out the current move once for that window, like 'up 6.2% from the low'. Every 'up' rule on that window with a percent of 6.2 or less fires. It's the same sorted-range trick as level rules.

Why one-second slots are fine

Using a second as the smallest step means the window edge can be off by up to a second. For a 10 minute rule nobody notices. Say it out loud as a trade-off, and offer smaller slots for short windows if the interviewer pushes.

Step 3: Firing each rule once, and not spamming (8 minutes)

Matchers can crash. When one comes back, it reloads its rules, replays the last hour of ticks from the stream to rebuild its windows, and carries on from its last checkpoint. That means it might fire the same rule twice.

So the notifier only sends after inserting the rule ID plus a firing number into a table with a unique key. The second insert fails and the duplicate is dropped. That's idempotency: doing it twice has the same effect as doing it once.

Repeating rules need care. A level rule should re-arm only after the price moves back past a small buffer, so a price bouncing around 100.00 doesn't send fifty pushes in a minute. A move rule stays true for a while once it's hit, so give it a cooldown, like not again until the window has fully passed.

Choices you should be able to defend

We choseInstead ofBecauseWe'd reconsider if
Rules held in memory per tickerQuerying a database for every tickMillions of database lookups a second would be slow and expensiveRules grew far past what memory can hold
Per-second low and highKeeping every tick in the windowFixed, small memory per ticker, and a second of slack is fine for alertsUsers wanted exact windows down to the millisecond
A fixed menu of window lengthsAny window the user typesEach ticker keeps a few running windows, shared by every rule on themUsers really needed odd windows, then round to the nearest second and accept more work
At least once, with a unique firing IDTrying to guarantee exactly once end to endMuch simpler, and the unique key gives the same resultNever

What could go wrong (5 minutes)

What happensWhat we do about it
A matcher crashesA new one loads its rules, replays the last hour of ticks to rebuild the windows, and duplicates get dropped by the unique key
A bad tick shows a 30% dropEvery move rule on that ticker fires. The price stream should hold back wild ticks, and the matcher can wait for a second tick that agrees before firing big moves
The market jumps and a million rules fire at onceFired events queue up and the notifier works through them. Matching never waits on sending
The push provider is slowRetry with growing gaps, and fall back to in-app messages
The price stream stallsThe windows stop moving. Don't fire move rules on the first tick after a long gap, since the 'move' might be hours old

Follow-up questions (5 minutes)

What about 'up 5% today'?

Turn it into a level rule when the day starts, since the opening price is known.

What about rules on volume, like 'twice the usual volume in 5 minutes'?

Same slots, but keep a sum of volume per second instead of a low and high. Sums are even easier to roll.

How do you show it was fast enough?

Track the gap between the tick's timestamp and when the push went out, and alert on the slowest 1% of sends.

How the 60 minutes add up

MinutesWhat you're doing
0–5Agree on what you're building
5–10Rough numbers
10–20Draw the overall design
20–30Checking level rules quickly
30–42Move rules without keeping the whole stream
42–50Firing each rule once, and not spamming
50–55What could go wrong
55–60Follow-up questions

Key concepts used here

Further reading

We wrote this sample answer ourselves.

Practice Loop is independent and isn't affiliated with or endorsed by Bloomberg.