Craps in Software and System Interview Example Questions
6 Example Questions
2 DSA · 4 System Design
Overview
Q1 · Stock IV
Q2 · Trading System
Q3 · Median Stream
Q4 · Distrib. Cache
Q5 · Rate Limiter
Q6 · Anomaly Detect
Cheat Sheet
Practice Plan

Why these 6 questions, and how to use this

2DSA Problems
4System Design
~45mPer Question
Trade-offs
The 6 Questions, Classified
Q1Max profit, ≤ k transactions. DSA. LeetCode 188. Tests dynamic programming maturity. State design is the whole game. Q2Real-time trading system, millions of TPS. System design. The flagship finance question — order book, matching engine, market data, audit log. Q3Median from a price stream in O(log n). DSA. LeetCode 295. Two-heap pattern; tests data-structure intuition and the ability to drop in the right specialised structure. Q4Distributed cache, strong consistency. System design. CAP, consensus (Raft/Paxos), quorum reads, leader leases. The "do you really understand consistency?" check. Q5Rate limiter — per-user, per-endpoint. System design with a coding lean. Token bucket vs sliding window; Redis + Lua for atomicity. Q6Real-time anomalous trading pattern detection. System design + ML. Tests stream processing (Kafka, Flink), detection layering, and surveillance/governance instincts.
The Meta-Signal Across All 6

What is being scored isn't whether you reach the "right" answer — it's whether you reason like an engineer who can be trusted with money.

How to Use Each Tab
📖
Read first
Each Q tab opens with the interview-room first move — what to clarify, how to frame, what to write down before code.
🧮
Solve second
DSA problems give progressive solutions (brute-force → optimal → space-optimised). Don't skip the brute-force narration in the room.
🏗️
Design third
System design walks the same template every time: clarify → estimate → architecture → trade-offs → failure modes. Memorise the template, not the answers.
🎯
Follow-ups
Each question has a "if they push you" section. The interviewer almost always pushes — these are the next 2-3 turns.
STAR bridges
Several questions have a "STAR connection" box — concrete ways to bridge to a project from your story bank when asked "have you done anything like this?"
📅
Practice plan
Last tab gives a 3-day study plan for these six. Don't try to absorb all of this in one sitting — it won't stick.
Note on framing. Question 2 ("real-time trading system, millions of TPS") and Question 1 of the existing Superday Prep ("Real-Time Order Book / Matching Engine") are very close cousins. Treat them as one body of knowledge — the questions overlap by ~70%. Same for Question 6 here and the existing PII-Detection design — both are streaming Kafka → Flink → ML/rules → audit log architectures.

Q1 — Max Profit with at most k Transactions

Classic DP. LeetCode 188 ("Best Time to Buy and Sell Stock IV"). The hardest of the six "stock" problems on LeetCode and the most general — every other variant collapses out of this one. If you can do it cleanly, you've shown DP fluency.

Problem (paraphrased)

You're given an array prices where prices[i] is the stock price on day i, and an integer k. You may complete at most k transactions, where one transaction = one buy + one matching sell. You can hold at most one share at a time. Maximise total profit.

DifficultyHard Pattern3D dynamic programming with state machine GeneralisesLC 121 (k=1), LC 122 (k=∞), LC 123 (k=2), LC 309 (cooldown), LC 714 (fee)
In the Room — First 90 Seconds
1
Restate & clarify
"At most k — so we may use fewer if it's not profitable, right?" "Can prices be negative or zero?" "Is k bounded? If k ≥ n/2, the constraint is essentially vacuous." "Define a transaction — buy + sell, counted once?" These three clarifications buy you 30 seconds and signal seriousness.
2
Walk a tiny example
Take k=2, prices=[3,2,6,5,0,3]. Buy at 2, sell at 6 → +4. Buy at 0, sell at 3 → +3. Total = 7. Doing this aloud convinces the interviewer (and yourself) that you understand the problem before you start coding.
3
Name the approach
"Brute force is exponential — every day has buy/sell/skip choices. I'll go to dynamic programming with a 3-dimensional state: day, transactions used, currently holding. Then optimise space."
4
Edge case first
"Quick edge: if k ≥ n/2, transactions are unbounded — I can just sum every positive day-over-day delta. That's O(n). Otherwise I'll do the full DP." This single line is what separates "studied" from "studied well."
The State Machine

On any given day, you're in one of two states: holding a share or not holding. From each state, two actions: do nothing, or transition. Count a transaction at the buy (one valid convention; sell-counting also works, just be consistent).

# State: dp[i][j][h] # i = day index (0..n-1) # j = transactions still available (0..k) # h = holding flag (0 = no share, 1 = holding) # # Transitions: # dp[i][j][0] = max( # dp[i-1][j][0], # rest, still not holding # dp[i-1][j][1] + prices[i] # sell today # ) # dp[i][j][1] = max( # dp[i-1][j][1], # rest, still holding # dp[i-1][j-1][0] - prices[i] # buy today (uses 1 txn) # ) # # Base cases: # dp[*][0][0] = 0 no transactions, not holding -> 0 # dp[*][0][1] = -inf no transactions left but holding is impossible # dp[-1][*][0] = 0 before day 0, not holding -> 0 # dp[-1][*][1] = -inf # # Answer: dp[n-1][k][0] (must end NOT holding to realise profit)
💡 The "convention question": do you decrement j on buy or on sell? Either is fine, but commit to one and say it out loud. Switching mid-derivation is where interviewers see candidates wobble.
Solution v1 — Naive 3D DP

Direct translation of the recurrence. Easy to read, easy to debug, gets full marks if you stop here cleanly.

def maxProfit(k: int, prices: list[int]) -> int: n = len(prices) if n < 2 or k == 0: return 0 # Edge: enough transactions = unconstrained if k >= n // 2: return sum(max(0, prices[i] - prices[i-1]) for i in range(1, n)) NEG = float('-inf') # dp[j][h]: best profit on current day with j txns left, holding h dp = [[NEG] * 2 for _ in range(k + 1)] for j in range(k + 1): dp[j][0] = 0 # start flat, no profit, no shares for i in range(n): new = [row[:] for row in dp] for j in range(1, k + 1): # not holding: rest, OR sell today new[j][0] = max(dp[j][0], dp[j][1] + prices[i]) # holding: rest, OR buy today (consume 1 txn) new[j][1] = max(dp[j][1], dp[j-1][0] - prices[i]) dp = new return dp[k][0]
TimeO(n · k) — every (day, txn) cell does O(1) work SpaceO(k) — only need last day's dp row
Solution v2 — Buy/Sell Two-Array Form

Equivalent — sometimes easier to write because the state names are intuitive ("max profit on day i after at most j buys, currently in state X"). Pick whichever you can write without bugs.

def maxProfit(k: int, prices: list[int]) -> int: n = len(prices) if n < 2 or k == 0: return 0 if k >= n // 2: return sum(max(0, prices[i] - prices[i-1]) for i in range(1, n)) # buy[j] = best profit after at most j buys, currently HOLDING # sell[j] = best profit after at most j sells, currently FLAT buy = [float('-inf')] * (k + 1) sell = [0] * (k + 1) for price in prices: for j in range(1, k + 1): buy[j] = max(buy[j], sell[j-1] - price) sell[j] = max(sell[j], buy[j] + price) return sell[k]
⚠️ Order of inner-loop updates matters — make sure buy[j] is updated before sell[j] in the same iteration; otherwise you'd allow buy and sell on the same day, which is fine here (zero profit) but in cooldown variants will be wrong. Walk the interviewer through this.
If They Push You — Likely Follow-Ups
  1. "Add a transaction fee." → Subtract fee on every sell: sell[j] = max(sell[j], buy[j] + price - fee). LC 714.
  2. "Add a 1-day cooldown after sell." → Now buy on day i requires sell happened ≤ day i-2. State becomes (holding, just_sold); or carry one extra row of dp. LC 309.
  3. "Now there are M stocks running in parallel — same k pooled." → Concatenating arrays doesn't work because order matters per stock. You'd merge in time order and add a "which stock" dimension. Discuss the curse of dimensionality and propose greedy heuristics if k is small.
  4. "Prices are streamed; recompute online as each new price arrives." → Hard problem. The DP is forward-only in i, so you can incrementally extend each day. Just keep the current buy/sell arrays, do one inner loop per new tick → O(k) per tick. This is a great answer; emphasise it.
  5. "What if k can also change between ticks?" → If k only increases monotonically, you can extend buy/sell with new entries (each new buy[j] initialised from sell[j-1] - price). If k can shrink, you have to discard and recompute from history — flag this.
  6. "Prove correctness of the edge case k ≥ n/2." → A transaction needs ≥ 1 day to buy + ≥ 1 day to sell. With n days, you can do at most ⌊n/2⌋ disjoint transactions. So when k ≥ ⌊n/2⌋, k is non-binding and the optimum equals the unconstrained optimum, which by a classic exchange argument = sum of positive consecutive deltas.
Common Mistakes That Cost the Offer
Practice (do these in order)
  • LC 121 (one transaction) — 8 minutes from cold start
  • LC 122 (unlimited) — 8 minutes
  • LC 123 (exactly 2) — 15 minutes, write the 4-variable form
  • LC 188 (this one) — 25 minutes, time yourself, then again from scratch the next day
  • LC 309 (cooldown) — 15 minutes, the cooldown extension
  • LC 714 (fee) — 10 minutes
Do all six in one week. The point is to see the family — by the end, LC 188 should feel like a special case, not a one-off.

Q2 — Real-Time Trading System, Millions of TPS

The flagship finance system-design question. "Design a system that can handle millions of transactions per second" is a deliberately broad prompt — the interviewer wants to see how you decompose, not whether you've memorised an answer. There is heavy overlap with the order book / matching engine prompt in the Superday Prep; treat this as the same body of knowledge with a different starting hook.

Mental model: what does "transaction" actually mean?

First clarification. "Transaction" is overloaded. It could mean:

"Millions per second" most naturally means order events. Confirm: "Are we sizing for ingress order rate, or for matched trades?" That single question signals you understand the domain.

Step 1 — Clarify (do this aloud)
Step 2 — Estimate

Always do back-of-envelope before architecture. Rough numbers:

Ingress5M orders/sec × ~200B per order = ~1 GB/sec wire-level (achievable with kernel bypass + 25/100 GbE). Matching shardsA single-threaded matcher comfortably does 100K–500K orders/sec for a hot symbol. 5M total / 200K per shard ≈ 25 matcher shards; round up to 50–100 for headroom & fault domains. Audit logEvery order persisted. 5M × 200B × 86,400 sec/day = ~85 TB/day raw. Compress + tier hot/warm/cold. Market data fanout100K subscribers × 5M ticks/sec = 500B msg/sec — only feasible via UDP multicast, never unicast.
Step 3 — Architecture
┌─────────────────────────────┐ Client ── FIX/binary ──→ │ Edge Gateway (per region) │ │ · TLS, auth, rate-limit │ │ · Pre-trade risk (fat-finger) │ · Sequence-number stamping │ └─────────────┬───────────────┘ │ shard by symbol │ ┌───────────┴───────────┐ ↓ ↓ ┌─────────────────┐ ┌─────────────────┐ │ Matcher (AAPL) │ │ Matcher (MSFT) │ ← single-threaded │ in-memory book │ │ in-memory book │ per-symbol leader └────────┬────────┘ └────────┬────────┘ │ │ └──────────┬───────────┘ ↓ ┌────────────────────────┐ │ Append-only event log │ ← Kafka (or LMAX-style ring buf) │ immutable, ordered │ SOURCE OF TRUTH └─────────┬──────────────┘ │ ┌────────────────────────┼─────────────────────────────┐ ↓ ↓ ↓ ┌──────────────┐ ┌────────────────────┐ ┌───────────────────┐ │ Market data │ │ Risk & surveillance│ │ Clearing/ │ │ publisher │ │ stream processor │ │ settlement │ │ (UDP mcast) │ │ (Flink/Kafka Strms)│ │ (downstream) │ └──────────────┘ └────────────────────┘ └───────────────────┘
The 5 Decisions That Define the System
A
Shard by symbol, single-threaded matching per symbol
Industry standard. Every serious exchange (NASDAQ INET, NYSE, LSE) uses this pattern. Single-threaded = no lock contention, no race conditions, deterministic for replay. The order book is two priority queues: bids (max-heap on price, then time) and asks (min-heap). Operations are O(log n) per insert; for hot symbols a sorted bucket-per-price-level array is even faster.
B
Append-only event log is the source of truth
In-memory book is a cache of the event log. On crash, replay the log from the last snapshot. This pattern (LMAX Disruptor, Kafka, event sourcing) gives you crash recovery, auditability, and the ability to spin up a parallel "shadow" matcher for upgrades — all from one design choice.
C
UDP multicast for market data, TCP/FIX for orders
Market data is broadcast to thousands of subscribers; unicast would be O(N) per tick. UDP multicast = one packet for all subscribers. Lossy by definition, so attach sequence numbers and have subscribers request gap-fill via TCP if they detect missing seq. This is exactly how NASDAQ ITCH/OUCH works.
D
Pre-trade risk in the gateway, post-trade risk in the stream
Pre-trade = synchronous (must finish before order touches matcher): basic checks like notional size cap, fat-finger, balance > required margin. Microseconds. Post-trade = asynchronous, in the stream processor: portfolio risk, VaR, exposure aggregation. Milliseconds to seconds is fine. Splitting these is non-negotiable; doing all risk synchronously kills your latency.
E
Fail-closed, not fail-open
If any critical service is unhealthy (auth, risk, log), reject the order. Never auto-approve "to keep things flowing." This is the cardinal rule that distinguishes finance from consumer software. Mention it explicitly.
Trade-offs to Verbalise (3+ minimum)
Failure Modes (the interviewer will probe)
Failure
Mitigation
Mode
Matcher crash
Replay event log from last snapshot. Standby matcher takes over. Snapshots every N seconds.
Fail-stop
Kafka unavailable
Reject new orders. Fail-closed. Don't accept orders you can't durably log.
Fail-closed
Network partition
Affected symbol(s) halted; healthy symbols keep trading. Per-symbol fault domains.
Isolate
Risk service slow
Circuit breaker → reject new orders requiring that risk check. Don't queue indefinitely.
Fail-closed
Market data lag
Subscribers detect via sequence number. Snapshot recovery via separate channel.
Detect & recover
"Locked / crossed" book (bid ≥ ask)
Pause symbol, alert. Should never happen but does on data feed bugs.
Halt
Vocabulary That Earns Points
💡 You don't need to know all these in detail. Sprinkling 2–3 of them naturally signals "I've read about real exchanges, not just System Design Interview Vol. 1."

Q3 — Median Price from a Stream in O(log n)

A pure data-structure problem. LeetCode 295 ("Find Median from Data Stream"), exactly. The reason this comes up in finance interviews is that streaming statistics — VWAP, rolling median, percentile bands — are bread-and-butter for trading dashboards and risk monitors.

Problem (paraphrased)

Implement a data structure with two operations:

Required: insert in O(log n), findMedian in O(1) ideally. The interviewer's required bound (O(log n) for both) leaves several solutions on the table.

Reasoning to the Two-Heap Trick

Walk it from naive to optimal. This narration is what gets you the offer; jumping straight to "two heaps" looks pre-memorised.

1
Naive: keep an unsorted list
Insert O(1), median O(n log n) via sort or O(n) via quickselect. Insert is fast but median is too slow on every call.
2
Sorted list (binary insert)
Insert O(n) (linear shift), median O(1). Now median is fast but insert is too slow at scale.
3
Self-balancing BST or order statistic tree
Both operations O(log n). Works, but a heavy structure for what we actually need.
4
Key insight: we only ever need the middle
We never query the 17th element or the 89th percentile here — only the middle. So we don't need full ordering. Split the stream into two halves, keep the bottom half's max and the top half's min cheap to access. Two heaps: max-heap for the lower half, min-heap for the upper half.
The Two-Heap Invariants

Maintain three invariants and the median is always free:

  1. Every element in lo ≤ every element in hi.
  2. Sizes balanced: |lo| - |hi| ∈ {0, 1}. (We let lo be the bigger one when odd.)
  3. Median: if odd total → lo.peek(); if even → (lo.peek() + hi.peek()) / 2.
import heapq class MedianFinder: def __init__(self): # Python heapq is a min-heap; for max-heap we negate values self.lo = [] # max-heap (negated): bottom half self.hi = [] # min-heap: top half def insert(self, x: int) -> None: # Step 1: tentatively put x in lo (negated) heapq.heappush(self.lo, -x) # Step 2: move lo's largest into hi (rebalance ordering invariant) heapq.heappush(self.hi, -heapq.heappop(self.lo)) # Step 3: rebalance sizes (lo should be ≥ hi by at most 1) if len(self.hi) > len(self.lo): heapq.heappush(self.lo, -heapq.heappop(self.hi)) def findMedian(self) -> float: if len(self.lo) > len(self.hi): return -self.lo[0] return (-self.lo[0] + self.hi[0]) / 2.0
insertO(log n) — at most 3 heap operations findMedianO(1) — peek both heaps SpaceO(n) — every value retained
💡 The "tentatively push to lo, then move max to hi, then rebalance sizes" pattern handles all cases including the empty-heap edge cleanly without nested ifs. Cleaner code = faster narration.
If They Push You — Likely Follow-Ups
  1. "Sliding window of size W instead of all-time median." → Now you also need removeOldest. Heap doesn't support arbitrary delete in O(log n). Two answers:
    • Lazy deletion: tag entries with index, mark removed, skip on peek. Amortised O(log n) but pathological cases.
    • Two SortedList / multiset (Python SortedList from sortedcontainers, C++ multiset): supports add and remove in O(log W). Cleaner. This is LC 480.
  2. "Now find the 95th percentile, not the median." → Two heaps generalise: keep the bottom 95% in one heap, top 5% in the other, maintain ratios on insert. Or use an order statistic tree (balanced BST with subtree size). Cleaner.
  3. "What if the stream is too big to fit in memory?" → You can't compute the exact median anymore. Approximate with:
    • t-digest — clustered quantile sketch, mergeable across machines. State of the art.
    • Greenwald-Khanna (GK) summary — bounded-error quantiles in O(log n / ε) space.
    • Reservoir sampling — uniform random sample, then take median of sample. Crude but trivial to implement.
    Mentioning t-digest by name is the easy way to look like you've worked with streaming statistics before.
  4. "How would you make this distributed across N shards?" → Each shard maintains its own sketch (t-digest), then merge centrally. The two-heap structure does NOT merge cleanly — you'd have to ship all elements. T-digest is mergeable; that's its whole reason for existing.
  5. "What if prices can be deleted by sequence number?" → Index-based lazy deletion in the heaps; or move to a balanced BST keyed by (price, seq_id).
  6. "What's the median of medians?" → Different topic — that's the deterministic O(n) selection algorithm. If they ask, redirect: "That's the linear-time selection trick — O(n) to find the kth element by partitioning around medians of 5-element groups. Different problem from the streaming version, but worth flagging."
Why Finance Cares About This
Practice (do these in order)
  • LC 295 (this one) — 15 minutes from cold
  • LC 480 (sliding window median) — 25 minutes; the lazy-deletion vs SortedList trade-off
  • LC 703 (Kth largest in stream) — 10 minutes; warm-up that uses one heap
  • Bonus: read 2 paragraphs of the t-digest paper. You don't need to implement it; just be able to say "it's a clustered quantile sketch, mergeable, used in DataDog and influxDB" if asked.

Q4 — Distributed Cache, Strong Consistency for Financial Data

A CAP-theorem question wearing a distributed-systems costume. The interviewer is checking whether you understand why "strong consistency" is in fundamental tension with availability under partition — and whether you know the standard tools (Raft, Paxos, quorum reads, leader leases) to engineer a usable CP system.

First clarification: do you actually want strong consistency?

In finance, the answer is yes for some data, no for others. Don't rush to "strong consistency" for the entire cache. State this clearly:

Strong neededAccount balances, position state, order status, cash ledger, P&L of record. If two reads disagree on a balance, money goes wrong. Eventual is fineMarket data ticks (with sequence numbers for gap detection), historical aggregates, dashboards, audit log views. Bounded staleness OKReference data (security master, FX rates that update every few seconds), risk dashboards refreshed every few seconds.

Saying "let me clarify which subset of financial data needs strong consistency" buys you 30 seconds and shows you don't blindly apply patterns.

CAP, Briefly — Mention by Name

CAP theorem: in a distributed system, under network partition, you cannot have both consistency and availability. Pick one. A strongly consistent cache is a CP system: it accepts writes only when a majority quorum of replicas can ack; if you're partitioned into the minority side, you reject writes.

⚠️ A common candidate trap: saying "I'll use Redis for strongly consistent cache." Redis is NOT strongly consistent by default. If you say that, the interviewer will ask follow-ups you can't answer. Either say "Redis with WAIT and Sentinel for bounded staleness — but if I need true linearisability I'd use etcd or build on Raft" or skip Redis entirely for this answer.
Step-by-Step Architecture
┌────────────────────────┐ write ────→ │ Cache Client SDK │ │ (knows leader per key)│ └─────────┬──────────────┘ │ partition by key hash → shard │ ┌───────────────────────┴────────────────────────┐ ↓ ↓ ┌────────────────┐ ┌────────────────┐ │ Shard 1 │ │ Shard N │ │ Raft group: │ │ Raft group: │ │ L F F F F │ │ L F F F F │ │ └─→ replicate │ │ │ │ log entry │ │ │ └────────────────┘ └────────────────┘ ↑ └─ Writes only ack'd to client AFTER majority quorum (3 of 5) durably writes the log entry. That's strong consistency.
The 5 Building Blocks
1
Consensus protocol — Raft (preferred over Paxos for clarity)
Raft elects a single leader per shard. The leader appends entries to a replicated log; entries become "committed" once a majority of replicas (e.g., 3 of 5) durably persist them. Production references: etcd (the canonical Raft impl), CockroachDB, TiKV, Consul. Paxos is older, more academic; Multi-Paxos and EPaxos are practical variants. Just say "Raft, à la etcd" and you're aligned with industry default.
2
Sharding — partition by key hash
One Raft group per shard. With 50 shards and 5 replicas each, that's 250 nodes. Adding a shard = rebalancing some keys, which is operationally expensive — design the hash space carefully (consistent hashing or rendezvous hashing).
3
Read paths — three options, pick deliberately
(a) Read from leader, with leader lease: the leader holds a time-bounded lease (say, 9 seconds renewable). Within the lease, reads are linearisable without contacting followers. Cheapest. Used in Spanner, Raft with optimisations.
(b) Quorum read: read from a majority of replicas, take the most recent. Always correct, but 1 RTT to multiple replicas — slow.
(c) Read from any replica with sequence-number freshness check: client supplies a "read-after" sequence number; replicas wait until they've caught up. Bounded staleness, not strict linearisability.
Default to (a). Be ready to discuss why.
4
Cross-shard transactions — 2PC or Calvin-style
A single financial operation often touches multiple keys (debit account A, credit account B). Single-key linearisability isn't enough. Two options: two-phase commit (2PC) across the involved Raft groups (slow, blocking on coordinator failure), or a deterministic transaction order (Calvin / FaunaDB-style) where transactions are sequenced upstream and replicas execute in lockstep. Spanner uses Paxos + 2PC + TrueTime for global transactions.
5
Failure handling
Leader fails → election (3-15 seconds typical) → unavailability window for that shard. Minority partition → can't commit, so rejects writes. Network split healing → log reconciliation; uncommitted entries on minority side are discarded. This is by design, not a bug — it's the price of strong consistency. Mention it.
Trade-offs to Verbalise
Reference Implementations to Name-Drop
System
Why it's relevant
Used by
etcd
Pure Raft KV store. Best canonical reference. Strong consistency, used as the metadata store of Kubernetes.
K8s, Cilium
ZooKeeper
Older. Uses Zab (Paxos variant). Strong consistency. Industry workhorse for coordination.
Kafka, Hadoop
Google Spanner
Globally distributed strong consistency using TrueTime (synchronised atomic clocks) + Paxos + 2PC. The gold standard.
Google internal
CockroachDB / TiKV
Open-source Spanner-likes built on Raft. Strong consistency at scale.
Many fintechs
FoundationDB
Apple's strongly-consistent transactional KV store. Famous for rigorous testing.
iCloud, Snowflake metadata
Redis Enterprise CRDB
Active-active geo-replication for Redis. CRDT-based, gives "strong eventual" guarantees, NOT linearisable.
Some HFT shops for caches
Likely Follow-Ups
  1. "Why is this not just Redis?" → Redis async replication = bounded staleness, not linearisable. Failover can lose the last few writes. Unacceptable for ledger data.
  2. "Walk me through Raft leader election." → Followers heartbeat from leader; if missed, follower becomes candidate, increments term, requests votes from peers. Majority vote wins. Term numbers prevent split-brain. Be able to draw this on paper.
  3. "What happens during a network partition?" → Side with majority retains a leader, keeps committing. Side with minority loses leader, can't commit (no quorum), serves only reads (stale). Heal: minority side replays log from majority leader, discards uncommitted entries.
  4. "Cache invalidation strategy?" → For a cache backed by an authoritative store, write-through (write to store + cache atomically — hard) or write-around with TTL (simple, but stale window). For a strongly consistent primary KV store, this question doesn't apply — there's no "source of truth" behind it.
  5. "How do you handle a clock skew between nodes?" → Raft itself doesn't depend on clocks for correctness — uses logical sequence numbers (terms, log indices). Clocks affect performance (election timeouts) but not safety. Spanner is the exception: it uses TrueTime with bounded clock uncertainty, requires GPS + atomic clocks in every DC.
  6. "Read-your-own-writes guarantees from a different datacenter?" → Either pin the client to the leader's region, or have the client carry a "read-after-seq-N" token that any replica can wait on. Sticky-by-session works in practice.
Don't confuse "cache" with "datastore." Many candidates default to "memcached / Redis" because the question says "cache." Push back gently: "When you say cache, do you mean a write-through cache backed by an authoritative store, or a primary KV store that's designed to be cache-fast? The architecture is different." This is a great clarifying question that sets up your whole answer.

Q5 — Rate Limiter, Different Limits per User and per Endpoint

A hybrid problem: half system design, half coding round. Token bucket and sliding window are the two algorithms you must know cold, and the implementation question almost always lands on Redis with Lua scripts because that's where the atomicity discussion lives.

First clarification
Algorithms — Know All Four, Pick Two
Algorithm
Behavior
Memory / Note
Fixed window counter
Count requests in each clock-aligned window (e.g., 0:00–0:01). Reset at boundary. Simple. Boundary burst attack: a user can fire 2× the limit by hitting just-before and just-after the reset.
O(1) per user. Cheap, weak.
Sliding window log
Store timestamp of every request in a sorted set. On request, prune entries older than window, count remaining, accept if < limit. Exact, no boundary issue.
O(N) per user where N = limit. Memory-heavy at scale.
Sliding window counter
Hybrid: keep counts for current and previous fixed window, weight previous by overlap fraction. Approximate but very good in practice.
O(1) per user. Best general default.
Token bucket
Each user has a bucket of capacity C, refilled at rate R. Each request consumes 1 token; reject if empty. Allows bursts up to C, sustained rate R.
O(1) per user. Best when you want bursts.
Leaky bucket
Requests enter a queue draining at fixed rate. No bursts. Good for shaping outbound traffic to fragile downstreams.
O(queue) per user.
💡 In an interview, name token bucket as your default with this reasoning: "It accommodates bursty real-world traffic, has O(1) state per user, and is what Stripe and most major APIs use." If asked for strictness without bursts, switch to leaky bucket. If asked for accuracy without storage cost, sliding window counter.
Architecture — Distributed
┌──────────────────────────┐ request ──→ │ API Gateway / middleware│ └──────────┬───────────────┘ │ │ key = user_id : endpoint ↓ ┌───────────────────────────────┐ │ Rate-limit logic (Lua-atomic)│ │ · fetch (tokens, last_ts) │ │ · refill = min(C, tokens + │ │ (now-last_ts)*R) │ │ · if refill ≥ 1: │ │ tokens = refill - 1 │ │ allow │ │ else: reject (429) │ │ · write back atomically │ └──────────┬───────────┬────────┘ │ │ ┌──────────┴────┐ ┌───┴────────────┐ │ Redis │ │ Config store │ │ (sharded) │ │ (limits per │ │ per-user │ │ user_tier × │ │ bucket state│ │ endpoint) │ └───────────────┘ └────────────────┘
Token Bucket — Reference Implementation

In-process Python first (interview-quality, ignores distribution):

import time from threading import Lock from collections import defaultdict class TokenBucketLimiter: def __init__(self, config): # config: {(user_tier, endpoint): (capacity, refill_per_sec)} self.config = config self.buckets = {} # (user, endpoint) -> (tokens, last_ts) self.lock = Lock() # single-process atomicity def allow(self, user_id, user_tier, endpoint): key = (user_id, endpoint) capacity, rate = self.config[(user_tier, endpoint)] now = time.monotonic() with self.lock: tokens, last = self.buckets.get(key, (capacity, now)) # Refill tokens = min(capacity, tokens + (now - last) * rate) if tokens >= 1: self.buckets[key] = (tokens - 1, now) return True self.buckets[key] = (tokens, now) return False

For a distributed deployment, replace the lock + dict with Redis + Lua:

-- Lua script, runs atomically inside Redis (single-thread server) -- KEYS[1] = "rl:{user_id}:{endpoint}" -- ARGV = capacity, refill_per_sec, now_ms local bucket = redis.call('HMGET', KEYS[1], 'tokens', 'last_ms') local capacity = tonumber(ARGV[1]) local rate = tonumber(ARGV[2]) local now_ms = tonumber(ARGV[3]) local tokens = tonumber(bucket[1]) or capacity local last_ms = tonumber(bucket[2]) or now_ms local elapsed = math.max(0, now_ms - last_ms) / 1000.0 tokens = math.min(capacity, tokens + elapsed * rate) if tokens >= 1 then tokens = tokens - 1 redis.call('HMSET', KEYS[1], 'tokens', tokens, 'last_ms', now_ms) redis.call('EXPIRE', KEYS[1], 3600) return 1 -- allowed else redis.call('HMSET', KEYS[1], 'tokens', tokens, 'last_ms', now_ms) return 0 -- rejected end
💡 The whole point of using a Lua script is atomicity. Without it, two concurrent gateways doing read-modify-write on the same bucket will race and both let the request through. Mention this race condition explicitly — it's what separates candidates who've actually built rate limiters from those who've only read about them.
Per-User × Per-Endpoint Configuration

Limits aren't constants — they vary by user tier and endpoint. Keep the config in a separate store, query it (cached) on each request:

# Example config — typical layered structure { ("tier=basic", "GET /quotes"): (capacity=100, rate_per_sec=10), ("tier=basic", "POST /orders"): (capacity=10, rate_per_sec=1), ("tier=premium", "GET /quotes"): (capacity=1000,rate_per_sec=100), ("tier=premium", "POST /orders"): (capacity=100, rate_per_sec=10), ("tier=internal", "*"): (capacity=10000,rate_per_sec=1000), } # Layered enforcement (composite limits): # global per-user cap (across all endpoints) # AND per (user, endpoint) cap # Both must allow. def allow_layered(user, endpoint): return bucket_global[user].consume() and \ bucket_per_endpoint[(user, endpoint)].consume()
⚠️ Order matters with layered limits. If you consume the global bucket and then the endpoint bucket fails, you've "spent" a global token unfairly. Solutions: (a) check both before consuming either, (b) compensate / refund the global on endpoint denial, or (c) accept the small over-spend (simplest). Mention the trade-off.
Trade-offs to Verbalise
Likely Follow-Ups
  1. "Race condition between two gateways?" → Lua script in Redis runs atomically. Without it, two reads + two writes race and both succeed when only one should.
  2. "What HTTP status do you return?" → 429 Too Many Requests. Include Retry-After header (seconds until next token available) and X-RateLimit-Remaining for client visibility.
  3. "How do you communicate limits to clients?"X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset response headers on every request, plus 429 with Retry-After on rejection. Documented in API docs.
  4. "What about geo-distributed users?" → Per-region limits with periodic global sync. Accepts that a user can briefly exceed their global limit by spreading requests across regions; flag the trade-off.
  5. "How would you protect against a single bad actor flooding the rate limiter itself?" → Defense in depth: drop traffic at the LB / edge before it reaches the limiter; per-IP fallback limit at the LB layer; circuit breaker on the limiter to fail-closed if Redis is overwhelmed.
  6. "Difference between rate limiting and throttling?" → Rate limiting = hard reject when over. Throttling = slow down, queue, or degrade quality. Different actions on the same signal.
Why finance interviewers like this question. Rate limiting in trading systems isn't just abuse prevention — exchanges require it. NYSE, NASDAQ, and most others impose order-rate caps per session (anti-spam, anti-quote-stuffing). Brokers must throttle clients to stay under exchange limits, and many regulatory regimes (MiFID II, SEC market-access rules) require pre-trade controls including rate limits. So this is a real production need, not just a textbook problem.

Q6 — Real-Time Anomalous Trading Pattern Detection

Surveillance is a real, critical engineering function inside investment banks. The question tests three things at once: streaming architecture (Kafka + Flink), ML-vs-rules tradeoffs, and your awareness of the regulatory / governance dimension that makes finance different from consumer ML.

First clarification
Patterns to Know by Name
Pattern
What it looks like
Detection signal
Spoofing
Place large orders one side of the book to push price, then cancel before fill and trade the other side.
High order-to-trade ratio, large orders cancelled near top-of-book, repeated pattern
Layering
Multiple orders at successive price levels to give false impression of depth, then cancel.
Many same-side orders cancelled within ms of placement
Wash trades
Trader buys and sells to themselves to inflate volume / paint a price.
Self-match: same beneficial owner on both sides; same algo, same machine
Marking the close
Trades concentrated in last seconds to manipulate closing print.
Volume / order count spike in last N minutes for thinly traded names
Momentum ignition
Rapid orders to trigger algo follow-on, then exit at the moved price.
Burst of aggressive orders + price move + flat position quickly after
Insider trading
Unusual volume in a stock just before material news.
Volume z-score spike + correlation with subsequent news event
Fat-finger / runaway algo
Order at unrealistic price (off by factor of 10), or thousands of orders/sec from a runaway loop.
Pre-trade risk: notional / price / rate vs daily-historical baseline
💡 Mentioning even 2-3 of these by name (spoofing, layering, wash trade) immediately moves you from "generic ML candidate" to "candidate who actually thought about finance."
Architecture — Layered Detection
┌──────────────────────────────────────────────────────────────────┐ │ Order events + trades + market data │ │ ──→ Kafka topic (partitioned by symbol or trader_id) │ └─────────────────┬────────────────────────────────────────────────┘ ↓ ┌──────────────────────────────────────────────────────────────┐ │ Flink stream processor │ │ │ │ Tier 1: deterministic rules (fast, microseconds) │ │ · price ≥ 10× last trade → fat-finger flag │ │ · order rate ≥ 1000/s/account → suspected runaway │ │ · self-match (same beneficial owner) → wash trade │ │ │ │ Tier 2: statistical (windowed, milliseconds) │ │ · rolling mean/stdev/MAD per symbol │ │ · z-score threshold; ARIMA forecast deviation │ │ · order-to-trade ratio over sliding window │ │ │ │ Tier 3: ML models (asynchronous, seconds) │ │ · isolation forest on multivariate feature vectors │ │ · autoencoder reconstruction error │ │ · sequence model (LSTM/Transformer) on order trajectories │ └─────────────────┬────────────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────┐ │ Action router │ │ · severity ≥ HIGH → kill order, page on-call│ │ · severity = MED → alert queue + freeze │ │ · severity = LOW → log + dashboard │ └────────────────┬────────────────────────────┘ ↓ ┌─────────────────────────────────────────────┐ │ Immutable audit log (S3 + Glacier) │ │ every decision + features + model version │ └─────────────────────────────────────────────┘
Why Layered, Not Just ML
1
Cost
Tier 1 rules cost CPU pennies. Tier 3 ML inference (especially on GPU) costs orders of magnitude more. Catching the obvious 80% with rules saves your ML capacity for the hard 20%.
2
Latency
Pre-trade kill must happen in microseconds. Only deterministic rules can do that. ML can be in the loop for post-trade flagging but rarely for pre-trade blocking.
3
Explainability & regulation
Under SR 11-7 (Federal Reserve model risk management) and similar regimes, ML models must be documented, validated, and explainable. A rule "blocked because price was 50× last close" is trivially explainable; an autoencoder anomaly score is not. Rules carry the load on must-explain decisions; ML supplements.
4
Adversarial robustness
Bad actors actively try to evade detection. Rules can be enumerated; ML models drift and can be probed. Layering means an attacker must defeat all tiers, not just one. Defense in depth.
Tech Stack to Name
ML Model Choices
Trade-offs to Verbalise
Governance Talking Points (Big GS Win)
Likely Follow-Ups
  1. "How do you handle concept drift?" → Monitor input feature distributions (KL divergence vs training set), monitor prediction distributions, alert on drift. Scheduled retraining + shadow validation. Mention data drift vs concept drift distinction.
  2. "How do you label training data?" → Hard problem. Confirmed manipulation cases are rare and lagged. Use weak labels (analyst-flagged + investigated), synthetic injection of known patterns, semi-supervised methods. Acknowledge the difficulty — interviewers respect honesty here.
  3. "What if the system itself is exploited?" → Detection thresholds become public via model probing. Defenses: rotate thresholds, add randomness, layered system means defeating one layer doesn't help, audit the auditor.
  4. "How would you scale this from one desk to firm-wide?" → Partition by trader_id × symbol; aggregate at desk and firm rollup levels. Per-desk Flink job, central aggregator, hierarchical thresholds.
  5. "What if a detection is wrong and you blocked a legitimate trade?" → SLA on false positive review (e.g., compliance reviews within 5 minutes), reversibility via cancel-and-resubmit, post-incident review every Friday. False-positive rate is a key SLO.
  6. "How do you detect coordinated manipulation across multiple accounts?" → Graph-based detection. Build trader/account graph; look for subgraphs with correlated trades, common beneficial owner, common IP / device fingerprint. ING Bank is on record using graph methods for this.

Cheat Sheet — One-Page Recall

Print this. The night before the interview, read this once, sleep, don't re-read in the morning.

Q1 · k-Transactions Stock
Approach3D DP — (day, txns_left, holding). Edge: k ≥ n/2 → sum positive deltas. State (terse)buy[j], sell[j] arrays; for each price: buy[j] = max(buy[j], sell[j-1] - p); sell[j] = max(sell[j], buy[j] + p). ComplexityO(n·k) time, O(k) space. TrapForgetting k ≥ n/2 shortcut → TLE.
Q2 · Real-Time Trading System
AnchorShard by symbol → single-threaded matcher per symbol → append-only event log → market data via UDP multicast. Latency layersPre-trade risk synchronous (µs); post-trade async via stream processor. Failure stanceFail-closed (reject orders) when log unavailable. Per-symbol fault domains. VocabTime-in-force (GTC, IOC, FOK), price-time priority, FIX/ITCH/OUCH, co-location, kernel bypass.
Q3 · Median from Stream
ApproachTwo heaps. Max-heap (lower half) + min-heap (upper half). Balance sizes within 1. InsertPush to lo (negated for max-heap); move lo's max to hi; rebalance sizes if hi > lo. Complexityinsert O(log n), median O(1), space O(n). ExtensionsSliding window → SortedList. Distributed → t-digest (mergeable). Memory-bounded → reservoir sample or t-digest.
Q4 · Distributed Cache, Strong Consistency
CAPCP system. Strong consistency forfeits availability under partition. AnchorRaft consensus (à la etcd) per shard. Majority quorum on writes. Leader leases for fast reads. Cross-shard2PC or Calvin-style deterministic ordering. Spanner = Paxos + 2PC + TrueTime (atomic clocks). Don't say"Just use Redis" — Redis is async-replicated, NOT linearisable by default. Referenceetcd, ZooKeeper, Spanner, CockroachDB, FoundationDB.
Q5 · Rate Limiter
Default algoToken bucket (capacity C, refill rate R). Allows bursts, O(1) state per user. AlternativeSliding window counter — most accurate at low memory cost. DistributedRedis + Lua script for atomicity (eliminates read-modify-write race). Layered limitsPer-user × per-endpoint config; both must allow. Response429 + Retry-After + X-RateLimit-* headers. FailureFail-closed for trading; fail-open for read-only.
Q6 · Anomaly Detection
AnchorKafka → Flink → tiered detection (rules → stats → ML) → action router → immutable audit. Recall vs precisionRecall wins; F-beta with β > 1. Tune threshold to favor recall. Patterns to nameSpoofing, layering, wash trade, marking-the-close, momentum ignition. GovernanceSR 11-7. Model versioning. Shadow deployment. Auditable decisions. DriftMonitor input distribution (KL divergence). Scheduled retraining + shadow validation.
Universal Verbal Habits
  1. "Let me clarify..." Always your first sentence. Buys 30 seconds, signals seriousness.
  2. "The trade-off here is X vs Y. I'd pick X because..." Minimum 3 trade-offs per system design.
  3. "Let me name the failure mode..." Don't wait to be asked.
  4. "In finance, fail-closed." If you only remember one phrase across all six, this one.
  5. "Append-only audit log." Mention once per system design.
  6. "I don't know — but here's how I'd figure it out..." Beats bluffing every time. GS scores this higher than confident wrong answers.

Practice Plan — 3 Days

These six are dense. Don't try to absorb them in one sitting. Spread the load. Each day combines reading, solving, and verbalising. Verbalising is not optional — most candidates fail interviews not because they don't know the material but because they can't narrate it under pressure.

Day 1 — DSA Day
AM
Stock IV (Q1)
Read the Q1 tab once. Then solve LC 121, 122, 123 from cold (45 minutes total). Then solve LC 188 — write the buy/sell two-array form, time yourself at 25 min. Don't peek. If you fail, look at the solution, close it, and re-write from scratch the same day. Then LC 309 (cooldown) and LC 714 (fee) as variations.
PM
Median Stream (Q3)
Read Q3 tab. Solve LC 295 (15 min). Solve LC 480 (sliding window median, 25 min). Read 2 paragraphs about t-digest — just enough to name-drop it convincingly.
Eve
Verbalise
Record yourself solving LC 188 out loud, from scratch, for 20 minutes. Watch it back. Note where you said "uh" and where you got stuck on convention. Re-record. This is the single highest-leverage exercise.
Day 2 — System Design Half-Day Marathon
AM
Trading System + Distributed Cache (Q2 + Q4)
Read Q2 tab. Then with a blank sheet of paper, draw the architecture from memory in 25 minutes. Compare to the diagram. Note what you missed. Read Q4 tab. Repeat the draw-from-memory exercise.
PM
Rate Limiter + Anomaly Detection (Q5 + Q6)
Same drill. Read tab → draw from memory → compare. For Q5, additionally implement the in-process Python token bucket from scratch in < 15 minutes. For Q6, list the 5 manipulation patterns by name and what signal would catch each.
Eve
Verbalise (mock interview style)
Pick 2 of the 4 system design questions. Record yourself going through CORE-R framework end-to-end (clarify, requirements, architecture, trade-offs, failure modes) at 35 min each. Watch back. Note where you started skipping clarification and went straight to architecture — that's the cardinal sin.
Day 3 — Integration + Cheat Sheet
AM
Cold-recall drill
Print the cheat sheet tab. Cover it with paper. For each of the 6 questions, write down on a blank page: the anchor approach, 3 trade-offs, and the failure mode. Compare to the cheat sheet. Whatever you missed, that's where to focus the rest of the day.
PM
Mock interviews
Pramp or a friend. Two 45-minute mocks: one DSA, one system design. Have them pick from these 6. Get external feedback on pacing, clarity, and trade-off habit.
Eve
STAR bridges
Q1 → state-space compression project (chess pipeline); Q2 → throughput pipelines you've built; Q6 → PII classifier. Practice the transition once each — "have you built anything like this?" → 90-second STAR.
What to NOT Do
Final nudge. Most candidates obsess about correctness ("did I get the right answer?") and underweight communication ("did the interviewer feel they were watching a senior engineer think?"). The bar at GS Superday is the second one. The cheat sheet is the floor; the verbal habits are the ceiling. Practice both.