InterviewLegend

Design a Leaderboard & Stock Ticker

Signature problem
Alex Chen38 min readUpdated 330 real interview reports
In this lesson
  • Work the ranking problem Bloomberg reports most often, from a two-operation API to a sharded design that never scans everyone
  • Say how a price ticker differs from a game leaderboard before the interviewer asks
  • Defend your choices on staleness and hot keys, and on what happens when updates outrun ranking

Understanding the problem

The prompt

Design the APIs for a leaderboard. One call updates a player's score, another returns that player's rank.

That is how it arrives. Two operations stated in a sentence, and the rest of the hour spent on what happens when the numbers get large. Candidate accounts of this round describe exactly that shape, and one records the same problem posed as stock ticking, which the interviewer described as being similar to a leaderboard.

Hold onto the second framing. It is the same computer science under different operating conditions, and noticing the difference unprompted is worth more here than any single design decision. We will build the leaderboard first, because that is what you are most likely handed, then convert it.

Assume a scale that makes the problem real without making it absurd. Ten million tracked entities, roughly fifty thousand score updates per second at peak, and around half a million rank queries per second. Those three numbers decide everything that follows, so state them early and check them with the interviewer rather than inventing them silently at minute forty.

Functional requirements

Do

    Don't

      Naming what you are skipping is not padding. It tells the interviewer you saw the surface and chose the part worth an hour, which is a different signal from not having seen it.

      Non-functional requirements

      Reason through these one at a time. Each buys or costs you a design option later.

      Read latency. Rank queries sit on a page load, so the budget is tens of milliseconds end to end. That rules out anything touching many machines per query, and it is the constraint that eventually forces the histogram in the deep dives.

      Staleness tolerance. The highest-leverage question in the round, and the one candidates skip. If a rank may be a second old you can precompute aggressively and serve from a cache. If it must be exact at read time, every update has to be visible to every subsequent query and you have signed up for a far harder system. Ask. In most products a second is fine, and getting that answer explicitly is what licenses the rest of the design.

      Write availability over write consistency. Dropping a score update is worse than applying it slightly late, so the write path should accept and acknowledge quickly and apply asynchronously. That is a real trade. Say which side you are taking.

      Read-write skew. Half a million reads against fifty thousand writes is ten to one. Every later decision leans on that ratio, so say the number rather than saying "read-heavy" and hoping.

      • 10M tracked entities.
      • 50k/s score updates at peak.
      • 500k/s rank queries at peak.
      • < 50ms rank query budget.

      The set-up

      Core entities

      1

      Entity

      The thing being ranked. A player, an instrument, an account. Carries an identifier and whatever display data the client needs. Small, changes rarely, never on the hot path.

      2

      Score

      A number attached to an entity, plus the timestamp it was set. The only thing changing fifty thousand times a second, and what everything else is derived from.

      3

      Ranking index

      A derived structure that can answer position queries. Not authoritative. Rebuildable from scores, and the part this lesson is really about.

      The split that matters is between the authoritative score and the derived index. Scores are the truth and belong somewhere durable, partitioned by entity identifier, in something ordinary like Postgres. The ranking index is a performance artefact that can be thrown away and rebuilt, which is what permits it to live in memory and to run slightly behind.

      State that separation early. Most of the hard deep-dive questions are about the derived side, and they get much easier once you've said aloud that it's derived.

      API

      POST /scores
      body: { entityId, score } # set outright
      body: { entityId, delta } # or adjust
      -> 202 Accepted # accepted, not necessarily applied yet
      GET /entities/{entityId}/rank
      -> { entityId, score, rank, asOf } # asOf is the freshness stamp
      GET /leaderboard/top?k=100
      -> { entries: [{ entityId, score, rank }], asOf }

      Two details there are doing real work. The write returns 202 rather than 200, which is you saying the update is durably accepted but not yet reflected in rankings. And every read carries an asOf stamp, which turns staleness from something you hide into something the client can see and decide about. Interviewers notice both.

      High-level design

      Here is the architecture we will spend the rest of the hour poking at. Blue is a client, violet is a service, amber is a log, and green is storage.

      Loading diagram...

      1. A score update arrives. The API validates it, appends it to the update log, and returns 202. It does not wait for any ranking structure to reflect the change, because that would put every downstream consumer on the write path and let the slowest one set your write latency. The durable write to the score store also happens off the log, so the truth and the index are fed by one ordered sequence rather than by two independent writes that can disagree. Punted for now: the client has no idea when its update becomes visible, which the asOf field partly answers and the staleness dive answers properly.

      2. The shards apply the update. Each rank shard owns a slice of entities by identifier hash and keeps a sorted structure over the scores it owns. Applying an update is logarithmic in the size of one shard rather than of the population, which is what makes fifty thousand writes a second survivable. Punted: the shards can now answer local questions perfectly and global questions not at all, which is the central problem here and the first deep dive.

      3. A top-K query arrives. The API asks every shard for its own top K and merges the responses, keeping the best K overall. That is correct because an entity in the true global top K must also be in the top K of whichever shard holds it. With a handful of shards and K around a hundred, this is a small scatter-gather well inside the latency budget. Punted: the cost grows with shard count, so at a hundred shards it stops being cheap.

      4. A rank query arrives. This one the shards cannot answer, because an entity's global position depends on how many higher scores exist everywhere else. Instead of asking every shard to count, the API reads a histogram of entities per score bucket. It sums the buckets above the entity and adds an exact count within the entity's own bucket. Punted: the histogram is approximate between refreshes, and hot entities still concentrate load. Those are the next two dives.

      Technology choices

      • Score store: Postgres, partitioned by entity identifier. A relational database. Scores are small structured rows with a natural key, read only by point lookups. And if you can rebuild the whole ranking layer from a boring durable table, everything above it becomes disposable. When it breaks: a primary failure means promoting a replica, and writes acknowledged but not yet replicated are lost. Since the log holds the real ordering, replay from the log after promotion rather than trusting the table to be complete.
      • Update log: Kafka, partitioned by entity identifier. A log rather than a work queue because several independent consumers need the same updates in the same order, and a queue hands each message to exactly one consumer. Partitioning by entity guarantees all updates for one entity land in one partition, so they apply in the order they happened. When it breaks: a broker failure triggers leader election on its partitions, consumers resume from their last committed offset, and duplicates arrive. Survivable here, because applying the same score twice is harmless when you set rather than increment, which is one good reason to prefer set semantics.
      • Rank shards: Redis sorted sets. An in-memory sorted set because it gives logarithmic insert and logarithmic rank in one structure, which is exactly the pair of operations needed, and because ranking is derived data that can be rebuilt. When it breaks: losing a shard loses no truth but does lose availability for its entities until it reloads. Reload from the score store, then replay the log from the offset recorded at snapshot time, which is why that offset must be captured alongside the snapshot.
      • Histogram: an in-memory count array, rebuilt periodically from shard summaries and updated by deltas in between. Not a database because it is tiny, hot and entirely derivable. When it breaks: rebuild it, and until then either serve ranks with a wider stated error or fall back to counting across shards for the queries you cannot answer.
      • Hot rank cache: Redis, holding recently requested ranks with a short expiry. A cache because request distributions are never uniform and a few entities take a large share of reads. When it breaks: everything falls through to the histogram path, which is slower but correct, so a cache failure degrades latency rather than correctness. Guard the stampede where one expiry triggers thousands of simultaneous recomputes by having a single request compute while the rest wait on it.

      Worth naming the log-versus-queue distinction explicitly, because interviewers ask. A work queue distributes tasks so each is handled once. A log retains an ordered sequence that many consumers read independently at their own pace. Ranking needs the second, because the shards, the histogram and the durable store each consume the same updates and must agree on their order.

      Potential deep dives

      How do you answer rank without scanning the population?

      This is the heart of the problem and where the round is usually decided. Everything else distributes cleanly. Rank does not, because it is a global question about counts held everywhere.

      BadCount higher scores on every read

      Approach

      Answer a rank query by counting every entity scoring above this one. One line of SQL, obviously correct, easy to explain.

      Challenges

      Linear in the population on every single read. At ten million entities and half a million queries a second this is not slow, it is impossible. Say it out loud anyway. Naming the obvious answer and why it fails earns you the right to the complicated one, and skipping straight to shards reads as recital.

      GoodOne sorted set holding everything

      Approach

      Keep every entity in a single skip-list-backed sorted set. Insert is logarithmic. Rank is logarithmic. Top-K is a range scan from one end. One structure answers all three operations with no coordination at all.

      Challenges

      It holds until the working set stops fitting on one machine or the write rate saturates a single process. At ten million small entries the memory is fine; it is the fifty-thousand-per-second write rate that breaks it. This is a genuinely good design for a smaller product, so present it as the baseline rather than a strawman, then name the specific boundary that forces the next step.

      GreatSharded sorted sets plus a score histogram

      Approach

      Shard the sorted sets by entity so writes distribute and each shard stays small. Then keep a separate histogram: an array of counts of how many entities fall into each score bucket. A rank query sums the counts of every bucket above the entity's score. Then it adds an exact count of higher scorers inside the entity's own bucket, which the owning shard answers cheaply because buckets are small.

      Challenges

      The costs are a second structure to maintain and a rank that is approximate between refreshes. Size buckets so one holds a manageable number of entities: with ten million entities, ten thousand buckets averages a thousand each, which keeps the in-bucket count cheap. Rebuild every few seconds from shard summaries, apply deltas in between, and state the resulting error bound rather than pretending it is exact.

      The reason this works is that rank does not require knowing who is above you, only how many. A histogram discards identity and keeps counts, which is exactly the information a rank query needs and a tiny fraction of the data.

      Say the error bound out loud. With a five-second rebuild and forty thousand updates a second, a rank could in the worst case be off by a couple of hundred thousand positions. In practice it's off by almost nothing, because updates rarely all move one way. Volunteer both the bound and why it is loose in theory and tight in practice.

      Work the arithmetic in front of the interviewer, because it is what turns this from a plausible idea into a design. Ten thousand buckets is eighty kilobytes as an array of 64-bit counts, so the whole structure fits in cache and a rank query is a summation over an array that never leaves the processor. Even summing every bucket naively is tens of microseconds. Keep a running prefix sum alongside it and the query becomes a single subtraction, at the cost of rebuilding the prefix sums whenever counts move, which is why the refresh cadence exists.

      Bucket sizing is the parameter to reason about aloud. Too few buckets and the exact in-bucket count gets expensive, because the owning shard has to count a large number of neighbours. Too many and the histogram stops fitting comfortably and the rebuild costs more. A thousand entities per bucket is a reasonable landing point: the in-bucket count is trivial, and the array is small enough to rebuild in milliseconds.

      Scores are rarely uniformly distributed, which breaks fixed-width buckets. In a game most players cluster near the bottom, so equal-width score ranges give you one enormous bucket and thousands of empty ones. The fix is to size buckets by population rather than by score width, computing the boundaries during each rebuild from the actual distribution. Mentioning that unprompted is a strong signal, because it shows you thought about the data rather than the algorithm.

      What happens when updates arrive faster than you can rank them?

      A game can slow its clients down. A market feed cannot be asked to wait, which makes this the ticker question in disguise and one Bloomberg reaches for.

      BadQueue everything and catch up later

      Approach

      Let the update log absorb the burst and process it in order at whatever rate the shards manage.

      Challenges

      The queue grows without bound and every value in it is obsolete before it is applied. You spend real work applying a price from four minutes ago to a structure serving live queries, and the lag never recovers because the arrival rate exceeded the service rate to begin with. An unbounded queue of stale data is worse than no queue.

      GreatConflate rather than queue

      Approach

      Keep a pending buffer holding only the latest value per entity. When updates for one entity arrive faster than they can be applied, the newer overwrites the older in the buffer, so a burst collapses instead of accumulating. The applier drains at whatever rate it sustains and always applies current values.

      Challenges

      This discards intermediate values, and whether that is acceptable is a property of the data rather than of the system. For a price or a score only the latest matters, so conflation is exactly right. For anything cumulative, such as a running total of events, dropping intermediates loses information and you must aggregate rather than overwrite. Say which you are assuming and why.

      Conflation also answers a related ordering question for free. Because you keep only the latest per entity, out-of-order arrivals within a burst stop mattering: compare timestamps, keep the newer, discard the older regardless of arrival order.

      The buffer is a map from entity to its latest pending value, which bounds its size by the number of distinct entities updated rather than by the number of updates received. That distinction is the whole reason conflation works. A burst of a million updates across ten thousand entities occupies ten thousand slots. You know the memory ceiling in advance, however bad the burst gets.

      Decide what the drain cycle looks like and say it. A fixed interval, say every fifty milliseconds, gives predictable latency and a predictable maximum staleness, which is easy to reason about and easy to state in the API response. Draining continuously gives lower latency when quiet and no protection when busy. The fixed interval is usually the better answer here precisely because the busy case is the one you are designing for.

      Do not conflate on the durable path. The log should still record every update, because the score store is the audit trail and because replay after a failure needs the real sequence. Conflation belongs between the log and the in-memory ranking structures, which is a distinction worth drawing explicitly on the whiteboard.

      Which entities break this, and what do you do about them?

      Request distributions are never uniform. In a game one player is trending; in a market one instrument is in the news. Either way a single entity can take a large share of all reads, and sharding does not help, because a single key lives on a single shard by definition.

      GoodCache the hot ranks with a short expiry

      Approach

      Put a small cache in front of the ranking API keyed by entity, with a time-to-live tied to your stated staleness budget. If a rank may be a second old, cache it for a second. A thousand requests for the same entity in that second collapse into one computation.

      Challenges

      Two failure modes worth naming. The stampede, where an expiry lets a thousand simultaneous requests all miss and all recompute; solve it by having the first compute while the others wait on that same computation. And the cold cache after a restart, where everything misses at once; warm it from the previous top-K if you have one.

      GreatCache the reads, conflate the writes

      Approach

      Reads are handled by the cache above. A hot key can't be spread across shards for writes, so reduce the work. Conflation already collapses repeated updates to one entity. A wildly active entity costs one apply per drain cycle, however many updates arrive.

      Challenges

      Be honest that a single logical key has a ceiling. If one entity still needs more write throughput than a shard sustains after conflation, you have two options left: split it into sub-keys and sum them, or accept a lower update frequency for it. Both are real answers. Pretending sharding solves it is not.

      How do you rebuild after losing a shard?

      In-memory ranking structures are the right call and they evaporate on restart. The interviewer will ask what happens, and the good answer is already implied by the architecture rather than bolted onto it.

      Because the shards are derived from the score store and the log, recovery is a reload followed by a replay. Load the current scores for the entities this shard owns. Then replay the log from the offset recorded when that snapshot was taken. The critical detail is that the offset must be captured with the snapshot rather than after it, or you miss the updates that landed in between.

      The question is reload time. Ten million entities across a handful of shards is a couple of million each, which loads in seconds rather than minutes. Say the number. Then say what happens during those seconds. Rank queries for that shard's entities either fail fast or fall back to a slower exact count against the score store. Picking one is a product call between absent-and-honest and slow-but-correct.

      Snapshot periodically so replay never starts from the beginning of retention. A snapshot every few minutes bounds recovery to a few minutes of log, which turns an unbounded worst case into a predictable one.

      There is a subtler failure worth raising: a shard that is alive but behind. It answers queries, so health checks pass, and every answer it gives is stale by however far it has fallen behind. Silent wrongness is worse than an outage because nothing alerts. Track consumer lag per shard and expose it. Have the shard refuse queries once lag exceeds the staleness budget you committed to in the requirements.

      That connects the whole design back to the number you established at the start. The staleness tolerance is more than a licence to precompute. It is the threshold that tells a lagging shard when to take itself out of service. Making that link explicit is the kind of thing that separates a design that hangs together from a collection of good components.

      How do you show ranks 1,000 to 1,100?

      Top-K is easy and it is not what users do. They scroll. Someone wants the page around their own position, or the hundred entries starting at rank one thousand, and the design so far cannot answer either without work.

      BadOffset into a merged global ordering

      Approach

      Ask every shard for its ordered entries, merge them, skip the first thousand, and return the next hundred.

      Challenges

      The merge has to materialise everything above the offset before it can skip it, so a request for rank one million does a million rows of work. Deep offsets are the classic pagination trap and they get worse the further a user scrolls, which is exactly backwards from what the user expects.

      GreatSeek by score, not by offset

      Approach

      Use the histogram in reverse. To find rank one thousand, walk the bucket counts until the cumulative total reaches a thousand, which lands you in a specific score bucket. Ask the shards for entries within that narrow score range, which is a bounded query each, then merge and slice the exact window. The work is proportional to the page size rather than to the offset.

      Challenges

      The page boundary is approximate, because the histogram is approximate. For a leaderboard that is fine: a user scrolling past rank one thousand does not audit whether the first entry is exactly one thousand or one thousand and four. Say that trade explicitly, because the interviewer wants to hear you noticing rather than hiding it.

      The same mechanism answers the more common product request, which is the page centred on the requesting entity. Look up that entity's score, take a range either side of it, and query the shards for that band. No offset arithmetic anywhere.

      This is the histogram's second job. Point it out in the interview. A structure you introduced to answer rank turns out to make seek-by-position cheap as well, because both questions are really about converting between a score and a count. Reusing a structure you already justified beats introducing a new one.

      How does this change when the scores are prices?

      The Bloomberg angle
      Change the nouns. Entities become instruments and scores become prices. Updates now arrive from a market feed rather than from players finishing games. Three properties invert, and naming them unprompted is the strongest signal available in this round. Reads dominate even harder, and they are latency-sensitive in a way a game is not. A stale leaderboard position is a cosmetic annoyance. A stale price is wrong information a professional may act on, so freshness stops being a performance metric and becomes a correctness one. There is no backpressure available. A game can queue submissions, or rate-limit its clients. A feed keeps arriving at whatever rate the market produces, which is why conflation here is the only workable answer, not merely an optimisation. The interesting query changes shape. Nobody asks for the rank of an instrument by price. They ask for a window: the top movers. Everything crossing a threshold. The instruments in one sector ordered by change since open. Still a ranking problem, but the sort key is derived and recomputed constantly rather than set by a client. That reshapes the design in three places. Keep ingestion and conflation as built. Swap the per-entity rank query for a few precomputed windows, and move the read tier closer to the client, since the latency budget tightens. The bones survive. The query surface changes.

      If you are handed the leaderboard version you do not need to build the ticker. You need to say, in a sentence or two near the end, how you would design it differently if the scores were prices. That one observation shows you thought about the product rather than recalling an architecture, which is what this interview is trying to find out.

      What is expected at each level

      Mid-level

      Roughly eighty percent breadth to twenty percent depth. You are expected to produce a working architecture, identify the sorted set as the right structure, and recognise that a single instance eventually stops being enough. You are not expected to reach the histogram unaided, but you should follow it when the interviewer leads you there.

      The bar: you can explain why counting higher scores on every read does not work, and you reach a sharded sorted set with a merged top-K without help.

      Senior

      Around sixty to forty. You should ask about staleness before designing, notice unprompted that rank does not shard, and propose something workable for it. You should also volunteer failure modes rather than waiting to be asked.

      The bar: you identify the rank-does-not-shard problem yourself and reach an approximate structure with a stated error bound, rather than insisting on an exactness you cannot afford.

      Staff and above

      Closer to forty to sixty. The architecture should arrive quickly and be treated as setup rather than answer, with most of the hour on trade-offs. Conflation versus queueing and when each is wrong. The hot-key ceiling. Recovery timing, and what accuracy is worth to the product.

      The bar: you reframe the problem yourself. Either explain how a ticker differs from a leaderboard before you're asked, or push back on the requirements. Establish that the product doesn't need exact ranking, then design for what it does need.

      Summary

      • ✓Ask how stale a rank may be before designing anything. It decides the architecture and it is the question candidates most often skip.
      • ✓Present the single sorted set as a legitimate baseline and name the boundary that breaks it, rather than jumping to shards to look sophisticated.
      • ✓Rank does not shard. A histogram of score-bucket counts answers it in time proportional to buckets rather than population, at the cost of being approximate between rebuilds.
      • ✓When updates outrun ranking, conflate rather than queue, and say plainly that this discards intermediate values and when that is acceptable.
      • ✓If the scores were prices, reads dominate harder and staleness becomes a correctness problem, with no backpressure available. Saying that unprompted is the strongest signal in this round.
      Had your Bloomberg interview?Tell us what they asked. Reviewed before it goes in, never with your name.
      Bloomberg interview guide

      Know what Bloomberg asks before you walk in.

      13 coding · 4 system design · 0 behavioral questions

      Get Premium$49 a year · 30-day money-back guarantee
      Or start freeNo card.

      Get their questions first.

      InterviewLegend is independent and not affiliated with or endorsed by any company it covers.