Trade Subscription Manager (LLD)
Low Level Design·advanced·~30 min read
- lld
- concurrency
- market-data
- design
- java
- bloomberg
What you'll learn
Problem Statement
Trading terminals and market-data desks need to distribute a live stream of trades to interested subscribers. A subscriber says "notify me every time a trade happens on AAPL with volume at least 5,000 shares." When a trade prints, every…
Step 1 — Understand the Query Patterns Before You Write Anything
Before drawing classes, get the shape of the load right. This is what separates a design that survives review from one that gets torn apart on the second question. Three method calls, three very different frequencies: | Method | Frequenc…
V1 — Naive, No Locks
Start with the smallest thing that could possibly work. One list of subscriptions, linear scan on every trade. This works — for a single thread, with no concurrent trades, and a small subscriber count. Say so out loud. The interviewer wa…
Follow-up 1 — What breaks when two threads call in at once?
Three concrete failures. Walk through each; don't hand-wave.
Failure A — ConcurrentModificationException mid-broadcast
Thread T1 is inside onTrade, halfway through iterating subs. Thread T2 calls subscribe (or a callback re-enters unsubscribe). The list mutates while T1 iterates. ArrayList's iterator detects the modification count changed and throws.
Failure B — Duplicate Handle.id
nextId++ is not atomic in Java. It compiles to: read nextId into a register, add 1, write back. Two threads reading the same value hand back the same id to two different subscribers. Now their Handles collide — one of them calling unsubs…
Failure C — Callback that unsubscribes itself
A common subscriber pattern: "notify me once, then stop." The callback calls TradeProcessor.unsubscribe(myHandle) from inside onTrade. In V1, that reaches back into subs.removeIf(...) while the outer onTrade is still iterating the same l…
The V2 fix
Guard every read and write with a single lock, and generate the id with an AtomicInteger. Two things worth pointing out to the interviewer: 1. Why the snapshot? If we invoked callbacks while holding the lock, a slow subscriber would stal…
Failure C revisited
Because Java's synchronized is reentrant per thread, the callback-driven unsubscribe in V2 doesn't deadlock. But even better, the snapshot approach means the unsubscribe mutates the real subs list while onTrade iterates the copy. The uns…
Follow-up 2 — The Hot Path Scans Everyone
V2 is correct. But onTrade's snapshot is O(N) in the total number of subscriptions across all symbols. On a broker with 10,000 symbols and 100 subscribers per symbol (1M total), every AAPL trade tick copies and scans a million-element li…
V3 — Bucket by Security.name
What we gained: onTrade for AAPL only scans AAPL subscribers. Cost went from O(totalsubs) to O(subsforthissymbol) — typically 3-4 orders of magnitude smaller. What got a little worse: unsubscribe no longer knows which bucket the Handle l…
Follow-up 3 — Within a Symbol, Most Callbacks Still Don't Fire
On a symbol like AAPL with 1,000 subscribers, a typical trade of size 500 might match only the 50 whose minSize ≤ 500. V3 still iterates all 1,000 to filter. The observation: a subscription with minSize = 10,000 cares about very few trad…
V4 — Bucket by minSize within each symbol
Replace List inside each symbol with a NavigableMap> — a TreeMap keyed by minSize, values are the subscriptions at exactly that threshold. onTrade calls headMap(trade.size, true) to get the sub-view of eligible entries and iterates only…
Follow-up 4 — Can We Drop the Single Lock?
V4 still funnels every onTrade through one synchronized block. On a machine with many cores and many symbols, the lock becomes the bottleneck — two AAPL trades and one GOOG trade all serialize on the same monitor even though they touch d…
Follow-up 5 — The Slow-Subscriber Problem
If one subscriber's onTrade callback takes 200ms (a rogue GC pause, a database write, whatever), every other subscriber for that trade waits behind it. On a hot symbol, one slow subscriber can back-pressure the entire distribution loop.…
Follow-up 6 — What About Reentrant unsubscribe in the Async World?
In V4 (sync), a callback calling unsubscribe reentered the same thread and worked. In V5 (async), the callback runs on a pool thread. It can call unsubscribe normally — the mutation goes through the concurrent-collections path, no reentr…
Where to Stop
For a 45-minute interview, V4 with ConcurrentHashMap + ConcurrentSkipListMap + CopyOnWriteArrayList is a strong stopping point. Bring up the executor and slow-subscriber problem as follow-ups if there's time. Don't try to draw V5 unless…
Summary of the Progression
| Version | State | onTrade cost | Concurrency | What it fixes | |---|---|---|---|---| | V1 | ArrayList | O(total) | None — broken | Baseline | | V2 | ArrayList + lock + AtomicInteger | O(total) | Single global lock | CME, duplicate id,…