Design an In-Memory Market-Data Store
- Treat the SIG design round as the range-and-category lookup store grown into an in-memory service
- Index by symbol, category, and numeric range, and know what "do it in O(1)" means here
- Turn an O(N) scan into indexed lookups, keep reads lock-free under ticks, and lay out memory for low latency
Understanding the problem
This is the movie-database problem that runs through the SIG coding loop, promoted to a design round. In the coding version you build a class that returns movies by genre and by a range of years. Here the objects are instruments instead of movies, and a price feed writes into the store while people read from it. The core is identical: pick the right in-memory representation so lookups stop scanning. If you have worked the range and category lookup store, you have already built the heart of this. The design round adds the service around it.
Read the shape before drawing anything. Everything lives in one process and in memory, because a trading system cannot pay a network hop or a disk read on the query path. The instrument universe is bounded, tens of thousands of equities, hundreds of thousands of option and future contracts, so the whole set fits in RAM many times over. Prices change constantly while the reference data behind them barely moves. That split, a small fast-changing layer sitting on a larger nearly-static one, is the design. Say it early and the rest follows.
Functional requirements
Keep the scope tight and let the interviewer widen it. The store answers three read shapes and takes one write, and naming that up front is half the design.
In scope
- Look up one instrument and its latest price by symbol
- Return every instrument in a category (asset class, sector, or exchange)
- Return every instrument whose numeric field falls in a range, for example strike between two values
- Apply a stream of price ticks that update each symbol's latest quote
Below the line (say it, then skip)
- Durability and replication across machines (the feed is the source of truth; rebuild on restart)
- Order-book matching or execution (this is a reference-and-quote store, not a matching engine)
- Historical time series and tick replay (only the latest quote is kept here)
- Authentication and multi-tenant isolation (an internal service inside the trading system)
- Cross-region distribution (one process, close to the strategies that read it)
Non-functional requirements
Attach a target to each one, because on this problem the non-functional side is the whole game. Read latency comes first. Strategies query the store on their hot path, so a lookup by symbol should cost a memory access or two, low microseconds, never a scan and never a lock that another thread holds. That single target drives almost every later choice. Reads never block, and reads never scan.
Write throughput is the second number. A busy feed pushes a large stream of ticks, tens to hundreds of thousands a second at peak, and each one updates one symbol's latest price. The writes are tiny and independent, so the pressure is rate, not size. The design question this raises is not "how do we store all of it," it is "how does a constant stream of updates coexist with readers that must not stall." Hold that thought for the concurrency deep dive.
Consistency here is per-instrument, and saying so precisely is worth points. A reader should always see one coherent quote for a symbol, its price and size and timestamp from the same tick, never a price from one update stapled to a size from the next. It does not need a single global snapshot across every instrument at one instant. Promising that would force coordination the problem does not need. Prices move independently, so per-symbol consistency is both cheaper and correct. Memory is a real constraint but a comfortable one. A few hundred thousand instruments at a few hundred bytes each is well under a gigabyte, so the job is laying it out for speed, not fitting it in.
- ~100K+ Instruments. Equities, options, futures; the full universe fits in RAM.
- low µs Read latency target. A lookup is a memory access, never a scan or a blocked lock.
- 10K–100K+/s Tick rate. Many tiny independent price updates, one per symbol.
- per-symbol Consistency. One coherent quote per instrument, not a global snapshot.
The set-up
Core entities
Name the few objects the store moves before drawing anything. There are three, and the split between the second and third is where the low-latency thinking starts.
Instrument
The reference record: symbol, assetClass (an enum like EQUITY or OPTION), exchange, and numeric attributes such as strike and expiry. This barely changes during a trading day, so treat it as nearly static.
Quote
The fast-changing part: price, size, and ts, the timestamp of the tick that produced it. One per symbol, overwritten in place on every tick. Keeping it separate from the Instrument is what lets a tick touch one small slot instead of a whole record.
Index
The lookup structures the store maintains: a map from symbol to instrument, a map from each category to its members, and a structure over each numeric field for range queries. These are derived from the instruments and rebuilt or maintained as the universe changes.
There is no database and no object storage anywhere. Most designs split huge bytes from small metadata and reach for a blob store for the bytes. Here there are no bytes and no persistence on the hot path at all. The store is pure in-memory state, and its source of truth is the feed. On startup it loads the instrument universe from a reference-data file or service, replays recent ticks, then serves everything from RAM. If the process dies, it rebuilds the same way. Durability is somebody else's problem by design, which is exactly why the store can be this fast.
Because it is all in memory, the "database" question becomes a data-structure question. The symbol lookup is a hash map from symbol to a slot. The category lookup is a hash map from category to a list of members. The range lookup is the interesting one, and the deep dive works through why bucketing usually beats a tree here. This is the same reasoning the System Design 101 course covers for Redis and in-memory stores; the SIG twist is that the store lives inside your own process, not behind a network call.
API
The interface is small and every method is called on the hot path, so keep it lean and return exactly what a strategy needs. The read methods do no work beyond a lookup; the write method is the only thing that mutates state.
The read methods each return the latest quote alongside the reference data, so a consumer gets a usable answer in one call and never has to make a second lookup for the price. That is a small choice that matters on a hot path: one call, one coherent record.
High-level design
The whole thing is one process, but it has two clearly separate paths inside it, and you should draw them apart. A write path takes the feed and updates state; a read path answers queries and must never wait on the write path. The diagram codes each box by role: blue for outside sources and consumers, violet for our components, green for the in-memory state. The walkthrough tags each piece the same way.
1) Ingest and normalize. The Exchange feeds push raw messages into the Feed handler and normalizer, which parses each one and maps it to a canonical internal tick: one symbol, one price, one size, one timestamp. This is where venue quirks get ironed out, so nothing downstream has to know that two exchanges spell the same instrument differently. What we keep off the hot path here is all the parsing and mapping cost; by the time a tick reaches the store it is already clean. The bottleneck being punted: the feed is unreliable at the edges and sends late or duplicate messages. Deciding when a tick is stale enough to drop is a real concern, handled in the reliability dive.
2) Apply the tick. The Store writer, a single thread, takes the normalized tick and overwrites the symbol's slot in the Latest-quote table with the new price, size, and timestamp. It does not touch the indexes, because the set of instruments has not changed, only one instrument's price has. That is the key move: a price update is a single write to one array slot and nothing else. What stays off the path is any index maintenance, which is why a tick is cheap no matter how fast they arrive. The bottleneck being punted: that write happens while readers are reading the same slot, so making the overwrite safe without a lock is the concurrency deep dive.
3) Serve queries. A consumer calls the Query API, which resolves the query through the Symbol, category, and range indexes to a set of symbol ids. It then reads each id's current quote from the Latest-quote table and returns coherent records. A symbol lookup is one hash hit, and a category lookup adds a read of the member list. A range query walks only the matching part of the range structure, never the whole universe. No query ever scans all instruments and no query waits on the writer. The bottleneck being punted: exactly how the range structure avoids a scan, and why a hashmap usually beats a tree here, is the first and most important deep dive.
Technology choices
Each box above is a role. Name a concrete structure for each and be ready to defend the category, because the follow-ups here are all "why that and not the fancier thing." The theme running through every choice: this is an in-memory data-structure problem, so the answers are containers and layout, not services and clusters.
- The symbol index is a plain hash map from symbol string to an integer id, and everything else keys off that id. Intern symbols to small integers once, at load time, and the quote table can be a flat array. A lookup becomes an array index instead of a hash of a string. The failure to plan for is a symbol that is not present, which just returns null; there is no partial state to corrupt because the writer only ever overwrites existing slots.
- The category index is a hash map from category to a list of ids. Categories are low-cardinality, a handful of asset classes and exchanges, so this is a tiny map whose values are member lists. When the universe changes you append or remove an id from a list, an O(1) or O(list) operation done off the hot path. Readers get the whole category in one hash hit plus a walk of the list, which is optimal since they asked for every member.
- The range structure is the one real decision. For a low-cardinality discrete field like expiry date or a strike ladder, use a hash map from bucket value to a list of ids. A range query iterates the handful of bucket values inside the range and gathers their members. For a high-cardinality continuous field, a sorted array of (value, id) with binary search, or a balanced tree such as C++
std::map, gives a clean range walk in O(log N + k). The deep dive works through when each applies; the point is to match the structure to the field, not to default to a segment tree. - The latest-quote table is a flat array indexed by symbol id, holding a small fixed-size quote per instrument. A flat array beats a hash map here because the access is always by a dense integer id, so there is no hashing and no pointer chasing, and the memory is contiguous and cache-friendly. This is the layer that changes on every tick, so its layout is where microseconds are won or lost.
- There is deliberately no external database, no message queue, and no cache tier. Adding Redis or Kafka to a single-process in-memory store is the classic over-build: it puts a network hop on the exact path that has to be microseconds. The feed itself is the durable log, and the store is the cache. Naming that you considered and rejected them, and why, reads far better than drawing them in.
- The language is C++ or Python, and the choice colors the answer. In C++ you talk about
structlayout, contiguous vectors, and avoiding allocation on the tick path. In Python you lean on dicts and lists and are honest that the concurrency story leans on the interpreter rather than fine-grained atomics. On the low-latency desks the expected answer is C++, and layout questions are fair game.
Potential deep dives
The skeleton is small on purpose. Nearly all the signal is in the next four questions, and the interviewer will pick one or two and push hard. The first two are where the round is usually decided.
1) What does "O(1) by category and by range" actually mean, and how do you get it?
Start by being honest about the "O(1)," because the phrasing is a trap and the interviewer is watching for whether you fall in. A lookup by symbol or by category really can be O(1) to find: hash the key, get the slot. But a query that returns every matching instrument cannot be literally O(1), because producing k results is at least O(k) no matter how you store them. So "do the range query in O(1)" cannot mean constant time end to end. What it means, and what you should say out loud, is O(1) to locate the matches, no full scan of the universe. The candidate who says "O(1) to find, plus O(k) to return the k results you asked for" has already passed the part of the question the interviewer cares about.
With that framed, the question is how to locate a range's matches without scanning all N instruments. There are three shapes of answer, and the reported failure mode is reaching straight for the most elaborate one.
Approach
Either build a segment tree over the numeric field, or just loop over all instruments on every query and keep the ones in range.
Challenges
The linear scan is O(N) per query, which is the exact thing the round asks you to fix. The segment tree is the more instructive mistake, and it is the one candidates make on the movie version. A segment tree answers range aggregate questions like sum or min, not "give me every element in this range." So it is both the wrong tool and far more code than the problem needs. Proposing it signals that you reached for a structure you had memorized instead of reading what the query asks. The interviewer on the movie problem said as much: the answer is a simple hashmap.
Approach
Keep a sorted array of (fieldValue, id) pairs for the range field. A range query binary-searches for lo, then walks forward until it passes hi, collecting ids. That is O(log N + k): log to find the start, then linear only in the number of results. It is a clean, correct answer and completely acceptable, especially for a continuous field like price where values are dense and mostly distinct.
Challenges
Maintenance is the catch. If instruments are added or an indexed field changes, inserting into a sorted array shifts elements, which is O(N). A balanced tree such as std::map fixes that with O(log N) inserts at the cost of pointer chasing and worse cache behavior. Either way the log factor is real, and for a discrete field with only a handful of distinct values in a typical range, you are paying for generality you do not use.
The sorted-array answer is genuinely good, and for many fields it is the right one. What pushes toward the next tier is a specific observation about the fields SIG queries: many of them are discrete and low-cardinality. Expiry dates fall on a monthly or weekly schedule. Strikes sit on a fixed ladder. When the number of distinct values a range can span is small and bounded, you can drop the log factor entirely.
Approach
For a discrete field, keep a hash map from each distinct field value to the list of ids that have it, a bucket per value. A range query iterates the values from lo to hi, and for each one does an O(1) hash hit to grab its bucket, concatenating the members. If a range spans a bounded number of buckets, say a dozen expiry dates, that is O(buckets + k), which is effectively constant plus the results. This is the hashmap the interviewer means. Then be explicit that the choice is field-dependent: use the bucketed hashmap when the field is discrete and low-cardinality, and fall back to the sorted array or tree when it is continuous and high-cardinality. Naming the boundary is the senior move.
Challenges
The bucketing only wins when the range spans few distinct values; over a huge continuous range it degenerates to visiting many buckets, which is why the field type decides the structure. Maintaining several indexes also means a change to an indexed field has to update every index that covers it, which the writer does off the hot path when the universe changes. And enumerating the values in a range assumes you can step through them, which is natural for dates and strike ladders but needs the sorted structure for arbitrary reals.
The scaling reads lesson covers indexing and read-path design in general. Here the trick is choosing the index that matches the field, the same judgment the coding range and category lookup store drills.
2) You are handed a naive O(N) version. How do you optimize it?
This is the most-reported SIG design-flavored round, and it deserves its own treatment because the framing differs from a blank-page design. You get working code and about thirty minutes to make it fast. Often it reads a CSV of records and exposes a search that loops over the list, filtering by category and range. The code already works and passes tests. Your job is to change how it stores the data, not what it computes.
Work it in a fixed order, out loud. First, read the access pattern: which queries repeat, and on which fields. A search called once is not worth indexing; a search called on every tick is. Second, move the cost from query time to load time. The naive version does zero work at load and O(N) work per query. Build the indexes once when the data loads, and each later query drops to a hash hit plus its results. Third, name the trade you just made. Indexes cost memory and make writes do a little more work. You spend both to buy fast reads, the right trade when reads dominate.
Approach
Parse the records once into memory, and while doing so build the symbol map, the category map, and the range structure from the first deep dive. Every query afterward goes through an index instead of scanning. A single pass at load turns O(N) per query into O(1) or O(log N + k), and for a workload of many queries that is the entire win.
Challenges
You now hold the data twice in effect, once as records and once as indexes, so memory goes up, and you have to keep the indexes correct as data changes. For a static CSV loaded once that second cost is zero, which is why the interview version is clean. In the live store it is the maintenance cost the concurrency and consistency dives handle.
What the interviewer grades is the diagnosis. You see that the data is not indexed for the query, then make the minimal change that fixes it, rather than rewriting the whole thing or adding structures the queries do not need. Say what you are measuring, change the storage, and re-state the new complexity. That narration is most of the score.
3) Ticks and reads hit the same data. How do reads stay fast without locking?
Here is the tension. The writer overwrites a symbol's quote while readers are reading that same quote. If a reader catches the write half-done, it could see a new price with an old size, which is a torn, incoherent quote and a correctness bug. The obvious fix is a lock around every quote. That is the one thing the latency target forbids, because a reader waiting for the writer's lock is a reader that stalls on the hot path. So the question becomes how to make the overwrite atomic enough that readers never tear and never block.
First, remove half the problem. Put all writes on a single thread, as in the design. Now there is no writer-versus-writer contention at all, and the only interaction left is one writer against many readers, which is a much easier problem than the general case.
Approach
Wrap every read and every write in one mutex so only one thread touches the store at a time.
Challenges
This serializes everything. Under a heavy tick stream, readers spend their time waiting for the writer to release the lock, and the microsecond read target is gone. It is correct and trivial to reason about, which is why you state it as the baseline, but it trades away the one property the whole design exists to provide.
Approach
Make each quote small enough to publish as a unit. Pack price, size, and timestamp so the writer can swap in a new quote with a single atomic store, and readers do a single atomic load. A reader either sees the whole old quote or the whole new one, never a mix, so quotes never tear, and neither side waits on a lock. Because updates are per symbol and independent, this scales with tick rate cleanly.
Challenges
Packing a full quote into one atomically-writable unit is easy in C++ with a small struct or a versioned slot, but awkward once the quote grows beyond a word or two. In Python the interpreter's own locking gives you atomic reference swaps but takes fine-grained control away, so you would say the quote object is replaced by reference rather than mutated in place. Either way you need the quote to be replaced as a whole, not field by field.
Approach
For quotes that are a few fields, use a sequence lock: the writer bumps a per-slot version counter before and after it writes. A reader reads the counter, reads the fields, then reads the counter again; if it changed or was odd, the write was in flight, so the reader retries. Reads take no lock and almost never retry, and they are guaranteed a coherent quote. The nearly-static indexes change only when the universe changes, so handle them differently. Build a new index and swap a single pointer to it atomically. Readers see either the old complete index or the new complete one. This is the read-copy-update idea: fast-changing data gets a seqlock, rarely-changing data gets a whole-structure swap.
Challenges
A seqlock assumes writes are short and rare relative to reads, which holds here since one tick touches one slot briefly; if writes were long the retries would pile up. The pointer swap needs safe reclamation of the old index, since a reader might still be walking it when you swap. Free it only once no reader can hold it. That is the fiddly part of read-copy-update. Be honest that this is bounded-staleness by nature: a reader can be looking at the index from an instant ago, which is fine because instruments do not appear and vanish tick to tick.
The handling contention lesson goes deeper on locks, atomics, and lock-free reads; the SIG-specific point is that separating the fast-changing quotes from the nearly-static indexes lets each use the cheapest scheme that fits it.
4) How do you lay the memory out for the low-latency desks?
Push here for senior and low-latency roles, where big-O stops being the interesting axis and constant factors take over. Two lookups can both be O(1) and differ by an order of magnitude in wall-clock time depending on how many cache lines they touch. The reference-data instinct is "an array of instrument objects, each with a pointer to its quote." That is the slow layout. Following a pointer to a quote that lives somewhere unrelated in memory is a likely cache miss, and a cache miss can cost more than everything else the lookup does.
Separate hot from cold, and lay the hot data out contiguously. Keep the latest quotes in one dense array indexed by symbol id, so a tick writes one slot and a symbol read touches one cache line, with no pointer to chase. Keep the colder reference fields elsewhere, since a price update never needs them. A range query scans a single field across many instruments. A struct-of-arrays layout, one contiguous array per field, lets that scan read only the bytes it needs, and the hardware prefetcher can run ahead. The alternative pulls whole instrument records to read one field from each.
Round it out with allocation discipline. Pre-size the containers at load so the tick path never allocates, and reuse buffers for query results instead of allocating per call. An allocation on the hot path is an unpredictable stall. Interning symbols to integer ids, from the first deep dive, pays off again here, since integer-indexed arrays are the most cache-friendly structure there is. None of this changes the asymptotic story, and all of it is what the low-latency desks listen for. Reaching this level unprompted is a strong senior signal; on an entry-level loop, naming that hot and cold data should be separated is already enough.
What is expected at each level
How far you carry the range structure, the concurrency, and the memory layout is roughly what sorts the levels. Here is what to land for the role you are interviewing for.
Associate / Software Engineer
Breadth-first, about 80/20. Reach a working store: hash map by symbol, hash map by category, a range structure that avoids the full scan, and a clear statement of what "O(1)" honestly means. Above all, read the query and choose the fitting structure instead of a segment tree, and be able to turn a naive O(N) search into indexed lookups. Naming that hot quotes and cold reference data should live apart is a bonus, not a requirement. The bar: a correct, right-sized in-memory design with sensible data structures and a clean interface, argued rather than recited.
Senior Software Engineer
Depth enters, roughly 60/40. You should reach the field-dependent index choice yourself, bucketing for discrete fields and a sorted structure for continuous ones, and defend the boundary. You should raise the reader-versus-writer problem unprompted and get to a lock-free read scheme, single-writer plus per-quote atomics or a seqlock, and explain why readers never block. Handle a bad or late tick as a first-class concern, not an afterthought. The bar: you find the concurrency and range-index deep dives on your own and defend the specific structures against the fancier alternatives.
Quant Developer / low-latency teams
Depth leads, roughly 40/60, and the constant factors become the conversation. Expect to reason about memory layout in detail, struct-of-arrays versus array-of-structs for field scans, cache-line behavior, symbol interning, and keeping allocation off the tick path. Market-data normalization across venues, safe reclamation of a swapped-out index, and how the store fits alongside an order book are all fair game. The bar: you own the latency argument end to end, from data structure to cache behavior, and the interviewer redirects you rather than rescues you.
Key Takeaways
- ✓This is the movie-database range-and-category problem grown into a service: an in-memory store, one process, no database on the hot path. Say that shape first and every choice follows.
- ✓"O(1)" means O(1) to locate, not O(1) to return k results. Get the lookup by symbol and category from hash maps, and the range from bucketing for discrete fields or a sorted structure for continuous ones, never a segment tree.
- ✓The optimize-the-naive-code round is design in disguise: diagnose that the data is not indexed for the query, build indexes at load, and make the minimal change. Restraint is the signal.
- ✓Keep reads fast under a tick stream with a single writer plus lock-free reads, a per-quote atomic or seqlock for the fast layer and a versioned pointer swap for the near-static indexes. Consistency is per-instrument, and saying so precisely earns points.
- ✓For senior and low-latency roles, layout matters most: hot quotes in a dense array, cold reference data apart, struct-of-arrays for field scans, and no allocation on the tick path.
Susquehanna interview guideKnow what Susquehanna asks before you walk in.
8 coding · 1 system design · 26 behavioral questions
Get their questions first.
InterviewLegend is independent and not affiliated with or endorsed by any company it covers.