Design fantasy sports contests (Dream11)
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
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
3 API
| Endpoint | What it does |
|---|---|
GET/v1/matches/{id}/players | Players, credits and roles; CDN cached. |
POST/v1/matches/{id}/teams | Create 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}/join | Join with {team_ids[]}; Idempotency-Key; debits wallet; returns contest instance and entry IDs. |
GET/v1/matches/{id}/my-scores | My 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_hashlockedWrite-heavy at the deadline; read by user. team_hash identifies identical teams, which are very common.
contest_instancesRedis counters + MySQL/Postgrescontest_idtemplate_idmatch_idsizefilledstatus (OPEN|FULL|LOCKED|SETTLED)prize_tableSpots 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_rankwinningsHundreds 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_keyDouble-entry; balance per user maintained transactionally with ledger insert.
player_pointsRedis + Postgres(match_id, player_id) → pointsbreakdownversionTiny (22 players per match); every scoring event updates it and bumps a version.
leaderboardsRedis sorted sets / computed snapshots in S3lb:{contest_id} → (entry_id, score); rank snapshots per versionSorted sets for small contests; histogram-based ranks for giant ones.
5 Architecture
Hover a box to highlight its connections.
6 Request flows
Join a mega contest 5 minutes before the deadline
- 1User picks a contest template ('₹1 crore mega') and two teams; the app sends POST /join with an Idempotency-Key.
- 2Contest service checks the deadline, team ownership and per-user entry limits (cached).
- 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).
- 4It calls the wallet service to debit the entry fee with idempotency key = join request ID; if the debit fails, it returns the spots.
- 5It emits EntryCreated to Kafka; an entry writer persists entries to the sharded database. The user sees 'joined' immediately.
A wicket falls
- 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).
- 2It publishes PointsChanged {match, version, player deltas}.
- 3Team score jobs recompute affected unique teams (by team_hash) as Σ points × multiplier, store scores by version, and refresh leaderboards.
- 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
- 1After the match, scorers verify final points (feed corrections often arrive); the match is marked COMPLETED.
- 2The settlement job computes final ranks per contest with tie rules (tied ranks split the combined prize of those ranks).
- 3Payout writes one winning ledger entry per entry with idempotency key = entry_id, in batches; reruns skip entries already paid.
- 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
Lessons this design uses
Practice questions
- The IPL final is in three weeks and your streaming and fantasy platform expects 10x normal traffic in a 30-minute window. How do you prepare operationally?
- In a fantasy cricket app, 2 million users are in one mega contest. Every ball changes player points, and users want to see their live rank. How do you compute and push leaderboard updates?
- One of your WebSocket gateway clusters crashes during the evening peak. Two million clients drop at once and try to reconnect, and the auth service and remaining gateways fall over. How do you survive this?
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.