Design problems
Booking & traffic spikesHardSDE-3 40 min

Design fantasy sports contests (Dream11)

Asked at
Dream11JioHotstarPaytmPhonePe

The interviewer asks

“Design a fantasy sports platform like Dream11. Before a cricket match, users create teams of 11 players within a credit budget and join contests, some with millions of spots. During the match, every team's points and its rank in each contest update live, and at the end the platform declares winners and distributes prizes.”

What a strong answer looks like

A strong answer identifies two very different peaks. The first is the join peak: tens of millions of users create teams and join contests in the last 30-60 minutes before the deadline (usually the match start), with contests that must fill exactly to their spot count. The second is the scoring peak during the match: every ball changes player points, and those must turn into updated scores and ranks for hundreds of millions of team entries. Joining uses a wallet debit plus a seat allocation on a sharded contest counter, with automatic creation of a new contest instance when one fills. At the deadline, teams lock. Live scoring computes player points once per event, then team scores as a weighted sum; ranks for a giant contest are computed in periodic batches using score histograms or sorted sets, not on every ball for every team. Final results and prize distribution are an idempotent batch over a ledger. Deep dives: contest joining at the deadline, the scoring and leaderboard pipeline, prize distribution, and pre-scaling for a known spike.

Concepts to mention

deadline spike and pre-scalingatomic spot counter per contest instanceauto-create contest instances when fullidempotent wallet debit tied to entryteams lock at deadline by server timeplayer points computed once per eventteam score as weighted sum; dedupe by team hashversioned points and scoreshistogram-based ranks for giant contestsidempotent settlement with tie rules

1 Requirements

Functional

  • Show upcoming matches, players and credits; create up to N teams per match (11 players, credit cap 100, role and per-team constraints, captain 2x and vice-captain 1.5x).
  • Browse and join contests (head-to-head, small leagues, mega contests with millions of spots) using a wallet balance; private contests with invite codes.
  • Edit teams until the deadline; teams lock at the deadline and become visible to other contest members.
  • During the match, show live player points, my team scores and my rank in each contest.
  • After the match, finalise points, compute final ranks with tie rules, and credit winnings to wallets.
  • Cancel or refund contests that do not fill (for non-guaranteed contests) or if the match is abandoned.

Non-functional

  • Join peak: ~10M+ concurrent users and ~1M team-or-join writes per minute in the final hour of a big IPL match.
  • Join API p99 under 500 ms; no contest overfilled; every join has a matching wallet debit.
  • Live points visible within ~10-30 s of the ball; leaderboard ranks may lag up to ~1 minute in giant contests.
  • Final results and prizes correct to the rupee; payout job idempotent and auditable.
  • Availability 99.99% around deadlines; graceful degradation (e.g. hide leaderboards) before failing joins.
  • Compliance with Indian law on real-money gaming (the 2025 Online Gaming Act bans real-money online games; the same design applies to free-to-play contests and to other markets).

Out of scope (say it out loud)

  • Payment deposits, withdrawals and KYC (mention only).
  • Player credit pricing model.
  • Social features (chat, follow).

2 Estimates

Users at peak~10-15M concurrentBig IPL or India match; most active in the 60 minutes before the deadline.
Team creations and joins~20k/s peak~200M team entries for one marquee match, ~50% in the last hour: 100M ÷ 3,600 ≈ 28k/s, with the last 10 minutes even hotter.
Mega contest size~2-5M spots per contestMega contests are filled in instances; a few dozen instances of popular formats fill per match.
Contest entries per match~200-500M rowsUsers join several contests with several teams; each (contest, team) is an entry.
Scoring events~300-600 per match~240 balls in T20 plus wickets, catches, run-outs, corrections; low rate, huge fan-out.
Team score recomputations~100M+ distinct teams per eventEach scoring event can change many teams; computing 100M dot products of 11 values is ~1B operations, a few seconds on a modest cluster.
Live score reads~200k/s10M users refreshing every ~60 s; served from cache.

3 API

EndpointWhat it does
GET/v1/matches/{id}/playersPlayers, credits and roles; CDN cached.
POST/v1/matches/{id}/teamsCreate or edit a team {players[11], captain, vice_captain}; validated against rules; rejected after deadline.
GET/v1/matches/{id}/contests?type=Contest templates with spots left (approximate); cached for seconds.
POST/v1/contests/{template_id}/joinJoin with {team_ids[]}; Idempotency-Key; debits wallet; returns contest instance and entry IDs.
GET/v1/matches/{id}/my-scoresMy teams' live points and ranks across contests.
GET/v1/contests/{id}/leaderboard?cursor=Ranked entries page; top ranks and 'around me' views.

4 Data model

teamsCassandra or DynamoDB (partition by match_id + user_id)
team_idmatch_iduser_idplayers[11]captainvice_captainteam_hashlocked

Write-heavy at the deadline; read by user. team_hash identifies identical teams, which are very common.

contest_instancesRedis counters + MySQL/Postgres
contest_idtemplate_idmatch_idsizefilledstatus (OPEN|FULL|LOCKED|SETTLED)prize_table

Spots allocated by atomic counter in Redis (DECR remaining) with the DB as durable record; new instance created when full.

contest_entriesSharded MySQL/Postgres or Cassandra (partition by contest_id, bucketed)
entry_idcontest_iduser_idteam_idjoin_txn_idfinal_pointsfinal_rankwinnings

Hundreds of millions of rows per big match; bucket giant contests into many partitions.

wallet_ledgerPostgres (sharded by user_id)
txn_iduser_idamounttype (deposit|join|refund|winning|bonus)ref_ididempotency_key

Double-entry; balance per user maintained transactionally with ledger insert.

player_pointsRedis + Postgres
(match_id, player_id) → pointsbreakdownversion

Tiny (22 players per match); every scoring event updates it and bumps a version.

leaderboardsRedis sorted sets / computed snapshots in S3
lb:{contest_id} → (entry_id, score); rank snapshots per version

Sorted sets for small contests; histogram-based ranks for giant ones.

5 Architecture

CLIENTSEDGESERVICESASYNCDATAclaim spotdebitEntryCreatedentry writerball eventsplayer pointsPointsChangedscores, ranksfinal rankscredit winningsLive match data feedFantasy appCDN + API gatewayLive score APIContest join serviceTeam serviceWallet serviceTeam score + rankjobsPoints calculatorKafka (joins, events)Settlement + payoutRedis (spots, points)Cassandra (teams)Sharded SQL (entries)Postgres (walletledger)
ClientEdge / gatewayService / workerQueue / streamCacheDatabase / storageAI / modelThird partyrequestasync (event / queue)

Hover a box to highlight its connections.

6 Request flows

Join a mega contest 5 minutes before the deadline

  1. 1User picks a contest template ('₹1 crore mega') and two teams; the app sends POST /join with an Idempotency-Key.
  2. 2Contest service checks the deadline, team ownership and per-user entry limits (cached).
  3. 3It claims spots atomically from the current open instance's Redis counter (DECRBY 2; if the result is negative, roll back and move to the next instance, creating one if the template allows).
  4. 4It calls the wallet service to debit the entry fee with idempotency key = join request ID; if the debit fails, it returns the spots.
  5. 5It emits EntryCreated to Kafka; an entry writer persists entries to the sharded database. The user sees 'joined' immediately.

A wicket falls

  1. 1The data feed sends a ball event: wicket, bowler X, caught by Y; the points calculator applies the scoring rules and updates player points for X and Y (with a version number).
  2. 2It publishes PointsChanged {match, version, player deltas}.
  3. 3Team score jobs recompute affected unique teams (by team_hash) as Σ points × multiplier, store scores by version, and refresh leaderboards.
  4. 4Users polling 'my scores' read their team scores and the latest rank snapshot from Redis; giant contests update ranks every ~30-60 s.

Settle the match

  1. 1After the match, scorers verify final points (feed corrections often arrive); the match is marked COMPLETED.
  2. 2The settlement job computes final ranks per contest with tie rules (tied ranks split the combined prize of those ranks).
  3. 3Payout writes one winning ledger entry per entry with idempotency key = entry_id, in batches; reruns skip entries already paid.
  4. 4Users get notifications; contest status becomes SETTLED.

7 Deep dives

Joining at the deadline without overfilling

Most joins happen in the last minutes, and a mega contest instance with 2M spots receives thousands of joins per second. A single database row with spots_filled would be a hot row. Use an atomic counter in Redis per contest instance (remaining spots), claimed with DECRBY; a negative result means full, so increment back and try the next instance. Because contest templates are fungible (users want 'a ₹49 contest with this prize structure', not a specific instance), the service auto-creates a new instance when one fills, which spreads the load and avoids a 'contest full' error. The wallet debit must be tied to the spot: claim the spot, debit with an idempotency key, and on debit failure release the spot. If the service crashes between the two, a reconciler compares claimed spots with debits and fixes them, so the Redis counter is fast but not the source of truth; the entry rows and ledger are. Entries are written asynchronously via Kafka to the sharded database, so the user path stays fast. At the deadline, the contest service flips match status to LOCKED in a config pushed to every node, and requests after that are rejected by server time.

Live scoring: compute once, fan out cheaply

Naively, every ball triggers a recalculation of 200M+ entries. The trick is to see that a team's score is a weighted sum of 11 player points (captain 2x, vice-captain 1.5x), and that there are far fewer distinct teams than entries: popular combinations repeat millions of times, so dedupe by team_hash. Pipeline: the points calculator turns each feed event into player point deltas (only 22 players matter) and publishes a versioned PointsChanged event. A distributed job (Spark, Flink or plain workers over partitions of unique teams) recomputes scores for unique teams containing changed players and writes them under the new version. Entries do not need their own score; they reference their team. This turns the problem into ~tens of millions of small dot products per event, which takes seconds on a modest cluster. Debounce: if several events arrive within a few seconds, process the latest version only. The user's 'my teams' view reads the team scores directly, so points feel live even when ranks lag a little.

Ranking millions of entries

Exact rank in a 2M-entry contest after every ball is expensive and unnecessary. Use different strategies by contest size. Small contests (2 to ~10k entries): a Redis sorted set per contest; ZREVRANK gives exact rank cheaply. Giant contests: compute ranks in periodic batches (every 30-60 s) using a score histogram. Scores are discrete (multiples of 0.5), so bucket entries by score; a user's rank is 1 + the number of entries with a higher score, a prefix sum over the histogram. The histogram for a contest is built from team-score counts weighted by how many entries use each team, which is cheap with the team_hash dedupe. Exact top-100 pages come from a sorted set of the leading entries only. Show 'rank is updating' instead of wrong ranks during recomputation, and version every snapshot so a client never mixes scores from one ball with ranks from another. Final ranks for settlement are computed once offline, exactly, with tie rules, and are the only ranks that move money.

Settlement and prize distribution

Money must be right even if the job crashes halfway. Settlement starts only after the match result and points are final, because data providers send corrections (a catch re-credited to another fielder) after the match. For each contest, compute final scores and exact ranks, then apply the prize table with tie handling: if three entries tie for ranks 4-6, they share the sum of prizes for 4, 5 and 6 equally. Write planned payouts to a settlement table first, verify totals per contest equal the prize pool (a hard check), then execute ledger credits in batches where each credit uses idempotency key = entry_id. Rerunning the job is safe: already-credited entries are skipped. For contests that did not fill and are not guaranteed, refund all entry fees with the same idempotent pattern. Large winnings may need tax deduction and withdrawal checks, handled by separate flows. Keep an immutable audit trail of inputs (points version, prize table) used for each contest.

8 Trade-offs

Redis counter vs database row for contest spots

A database row with conditional update is the simplest correct answer but becomes a hot row under thousands of joins/s per contest. A Redis counter is fast and atomic; its weakness is durability, solved by treating entries and ledger as truth and reconciling. Auto-creating instances reduces contention further.

Exact live ranks vs periodic approximate ranks

Exact ranks after every ball for every giant contest cost a lot and users do not need them second by second. Exact ranks for small contests and periodic histogram ranks for giant ones give a good experience at a small fraction of the cost. Final ranks are always exact.

Pre-scaling vs autoscaling

Match schedules are known weeks ahead, and the spike rises in minutes, faster than autoscaling reacts. Pre-scale compute, databases and caches per match tier an hour before, based on forecasts, and scale down after. Autoscaling remains a backup.

9 Bottlenecks

Wallet debits at the deadline

Every join debits a wallet; a user joining 20 contests in one minute creates contention on their balance row. Shard the ledger by user_id, keep each debit a single-row conditional update plus an insert, and batch multiple joins from one request into one debit.

Read storms during the match

Every wicket sends millions of users to refresh. Serve player points and team scores from Redis and CDN with short TTLs keyed by version; leaderboards serve precomputed pages. Degrade by slowing rank refresh before affecting scoring.

Team writes before the toss

Lineups are announced at the toss, ~30 minutes before the match, and millions of users edit teams at once. Teams are partitioned by user, writes are idempotent replacements, and validation is in memory with cached rules.

10 Failure handling

Data feed delays or sends wrong events

Use two data providers and compare; a scorer console lets operators correct events. Points are versioned, so a correction simply produces a new version and scores recompute. Settlement waits for a verified final version.

Redis node holding contest counters fails

Replica promotion may lose the last few decrements. A reconciler recounts entries per instance from the database and fixes the counter; if an instance is overfilled, the extra entries are moved to a new instance of the same template before the deadline, or refunded.

Match abandoned

Mark the match ABANDONED and run the refund flow over all entries with idempotent ledger credits. Users see the refund in their wallet; nothing is settled.

11 What impresses

  • Separates the join peak and the scoring peak and designs each differently.
  • Dedupes identical teams by hash to cut scoring work by orders of magnitude.
  • Uses score histograms to compute ranks in giant contests.
  • Treats Redis counters as a fast path with reconciliation against entries and ledger.
  • Waits for final verified points before settlement and handles ties correctly.
  • Pre-scales from the match calendar instead of trusting autoscaling.

12 Mistakes

  • Recomputing every entry and sorting every contest on every ball.
  • A single database row for contest spots under thousands of joins per second.
  • No link between spot claim and wallet debit, causing free entries or lost money.
  • Settling from live points before corrections arrive.
  • Assuming autoscaling will handle a spike that rises in 10 minutes.
  • Ignoring tie handling in prize distribution.

13 Follow-ups

Q1How do you show 'spots left' on contest cards without hitting Redis for every view?
Q2How would you support private contests with 10 friends?
Q3How do you stop users from creating thousands of accounts to win more?
Q4What changes for a football match with substitutions?
Q5How do you test the system before the IPL final?
Q6How would you add a live 'team vs team' comparison view?

Saved on this device. Create a free account to keep it on every device.

Practise this with Sam

A real mock interview, out loud, with an honest scorecard. Free to start.

Start free