Want it harder? Hide the model answer and work each phase in a scratchpad against its time budget, then reveal and score yourself. Or simply read on: the checkpoints below work either way.
How Do You Let Fifteen Million People Change Their Minds in the Same Thirty Minutes?
It is a Sunday evening in April 2025 in Chennai. Karthik Rao has been building teams for tonight's match at Chepauk since lunch: three of them, each eleven players inside a budget of 100 credits, each with a captain who scores double and a vice-captain who scores one and a half times. At 7:00 pm the coins go up. At 7:01 the playing elevens are announced, and the bowler Karthik made captain in two of his teams isn't in his side's eleven.
He has until 7:30, the scheduled start, to fix it. So does everyone else. Millions of phones open the app in the same minute to read the same two team sheets, re-pick their captains and join contests they were waiting to join until the lineups were known. At 7:29:58 Karthik saves his last edit and joins the biggest contest of the night, one entry among millions. At 7:30:00 every team in the match must freeze, exactly, everywhere. Then, for three and a half hours, every ball bowled changes the points of a few players, and with them the score and rank of tens of millions of teams.
- 200 million+ users (Dream11, October 2023)
- 15.01 million concurrent users on the first day of IPL 2024 (Harsh Jain on X, reported by Inc42, 22 March 2024); 10.56 million at the IPL 2023 opener (Dream11 Engineering)
- 308 million requests a minute in IPL 2023, "about five million requests per second" (Dream11 Engineering, May 2023)
- 8,000+ contest joins a second, 18 million+ contestants in a single match, 7.9 million+ entries in a single contest, 60 million requests a minute (Dream11, IPL 2020)
- 5 days, 2 hours, 1 hour — traffic is forecast 5 days ahead, re-forecast 2 hours before the match, and capacity is added from 1 hour before (AWS and Dream11, 2023)
- 40 million records in a 60-second SLA — leaderboards for about a million contests of 2 to 400,000 teams, recomputed every minute (Dream11 Engineering, 2017)
For context:
Dream11 paused all paid contests in August 2025, as Parliament passed the Promotion and Regulation of Online Gaming Act, 2025; it now runs free-to-play contests (see "Why Dream11 Matters")
This quest designs the system as it ran at IPL scale; the spike, the joins, live points and leaderboards are the same for free contests, and the wallet deep dive describes how prize money is handled where paid contests are lawful
Dream11 publishes peak numbers and parts of its architecture; everything else here, and every number labelled as an assumption, is an illustrative design
This is the system we'll design today.
Dream11 reported 15.01 million concurrent users on the first day of IPL 2024 and 308 million requests a minute (about 5 million a second) in IPL 2023; in IPL 2020 it took 8,000+ contest joins a second, 18 million+ contestants in one match and 7.9 million+ entries in one contest. On stated assumptions a big match has ~50 million teams and ~100 million contest entries, ~22,000 joins a second between the toss and the deadline, and a ball every ~45 seconds. Capacity is forecast days ahead and added an hour before the match, lineups are pushed to the CDN before the notification, and the gateway sheds low-priority routes so team saves and joins keep their capacity. Mega contests take spots from sharded counters that can never go below zero, and small contests are templates that always keep an open copy; a join id makes the spot, the wallet debit and the entry safe to retry. The deadline is one state flip on the match, checked atomically by every write, after which teams freeze into distinct lineups. Each ball recomputes player points from versioned records, rescores only lineups containing those players (captain 2x, vice-captain 1.5x), and rebuilds each contest's points histogram, so a rank is one lookup and ties share a rank. Settlement credits prizes by keyed postings from the verified scorecard, and withdrawals deduct 30% TDS on net winnings. Dream11 paused paid contests in August 2025 under the Online Gaming Act and now runs free contests. The deep dives end with a wrong forecast, a feed correction, an abandoned match and a rain-delayed final.
The Interview Begins#
You're interviewing at a sports-tech company. The interviewer shares two screenshots from the same phone: a team-edit screen at 7:29 pm with "Deadline in 00:01:12" in red, and a leaderboard at 9:40 pm reading "Rank 1,84,210 of 79,00,000 · 412.5 pts".
"Between these two screens are thirty minutes when nearly everyone writes, and three hours when nearly everyone reads. Design the fantasy platform: teams, contests, the deadline, live points, leaderboards and the money."
Before a match, users create teams (11 players from both sides, within 100 credits, with a captain and a vice-captain) and join contests: head-to-head, small leagues, and mega contests with millions of spots. At the deadline every team freezes. During the match, every player earns points for what happens on the field; every team's score and rank in each of its contests update live. After the match, results are verified and contests are settled.
Requirements
- Create and edit up to a fixed number of teams per user per match, until the deadline
- Join contests of 2 to 10 million spots; never sell more spots than a contest has
- Freeze every team at the deadline, exactly, even if requests are in flight
- Score every player after every ball; rescore every team; re-rank every contest
- Show each user their teams' ranks within about a minute of each ball
- Settle contests to exact final ranks; credit each prize exactly once
"Video streaming of the match is someone else's problem, and so is the score commentary page. Leave out the social features and the recommendations of which contest to join."
Phase 1: Requirements Clarification (5 minutes)#
"The shape of the load decides everything here, so I'll ask about time first: when does the traffic come, and when do writes stop?"
Your Questions#
"How big is the peak?"
"Dream11's CEO posted 15.01 million concurrent users on the opening day of IPL 2024. Its engineering team wrote that the IPL 2023 opener reached 10.56 million and that the platform executed 308 million requests a minute, about five million a second."
"When does it peak?"
"Around the toss. Dream11's engineers wrote that after the toss of every match they 'typically see a high surge in contest joins per second', with user concurrency around 5 million post-toss in 2021. The playing elevens are announced at the toss, about half an hour before the scheduled start, and the deadline is the scheduled start."
"How big is a contest?"
"Anything from 2 to millions. In IPL 2020 Dream11 reported more than 7.9 million entries in a single contest and more than 18 million contestants in a single match, with 8,000 contest joins a second."
"How fresh must the leaderboard be?"
"Dream11's 2017 leaderboard post set a 60-second SLA: it scored, sorted and ranked 40 million records for about a million contests every minute. Assume the same freshness at today's size."
"And the money?"
"Entry fees come from a wallet; winnings go back into it and can be withdrawn. In India, 30% tax is deducted at source on net winnings from online games, and since October 2023 GST is charged at 28% on the full amount deposited. Design it as it ran; paid contests have been paused since August 2025."
Functional Requirements#
- Teams: 11 players within 100 credits, a limit per side, one captain (2× points) and one vice-captain (1.5×); several teams per user per match; edits allowed until the deadline
- Contests: fixed spots and a fixed entry fee; templates that spawn a fresh copy when one fills; a join debits the wallet and takes one spot, once
- Deadline: at the scheduled start, every team and every join for the match freezes; a delayed start moves the deadline
- Live points: every ball from the data feed updates player points; every team and every contest re-ranks within a minute
- Settlement: after the match is verified, final ranks with ties, prizes credited once, tax deducted on withdrawal
Non-Functional Requirements#
Peak (published and estimated)
- ~15 million concurrent users, ~5 million requests a second (published)
- ~23,000 contest joins a second at peak, designed for 70,000 (estimated below)
Latency (design targets)
- Team save and contest join p99 < 300 ms, even in the last minute before the deadline
- A ball reflected in every user's rank within 60 s at p99
Correctness
- Zero edits accepted after the deadline; zero oversold contests
- Zero prizes credited twice; the ledger balances to the rupee
Phase 2: Back of the Envelope Estimation (5 minutes)#
"Three published anchors: about 15 million concurrent users, 308 million requests a minute, and 8,000 joins a second in 2020 at 5.3 million concurrent. Everything else, teams per user, the share of joins after the toss, distinct lineups, is my assumption, and I'll label it."
Before the numbers: what does one concurrent user cost?
Dream11 served 308 million requests a minute with 10.56 million concurrent users. How many requests a second is that, and how often does each user's app send a request? In 2020, 8,000 joins a second came with 5.3 million concurrent. If joins scale with concurrency, what do 15 million concurrent users imply? And how does the join rate compare with the total request rate?
One strong answer
- Requests: 308 million ÷ 60 ≈ 5.1 million a second. Per user: 5.1 million ÷ 10.56 million ≈ 0.5 a second, a request every ~2 seconds per open app.
- Joins: 8,000 × (15 ÷ 5.3) ≈ 23,000 joins a second at a 2024-sized peak. Design for 3×: ~70,000 a second.
- Ratio: 23,000 ÷ 5.1 million ≈ 0.5%. Almost all traffic is reads: contest lists, team previews, lineups, scores.
- So the writes are a small share of requests, but they all land in the same 30 minutes on the same few contests, and every one of them must be exact.
The Toss-to-Deadline Window#
| Value | |
|---|---|
| Concurrent users at the toss (published, IPL 2024 opener) | ~15 million |
| Users who play the match (assumed; 18 million+ in 2020) | ~25 million |
| Teams per user (assumed) | ~2 → ~50 million teams |
| Contest entries per team (assumed) | ~2 → ~100 million entries |
| Share of entries made between toss and deadline (assumed) | ~40% → ~40 million in 30 minutes |
| Average join rate in the window | 40 million ÷ 1,800 s ≈ ~22,000 a second |
| Team saves in the window (assumed 1.5× joins) | ~33,000 a second |
Conclusion: the window's average already matches the scaled-up published peak; the last five minutes are steeper still. Capacity for joins and saves must be in place before the toss, not added during it.
Live Points and Ranks#
- Deliveries: a T20 match is 240 legal balls plus extras, ~250 events in about 3 hours 20 minutes with the break: one every ~45 seconds
- Players touched per ball: usually 2 or 3 (batter, bowler, maybe a fielder) out of 22
- Teams to rescore per ball: up to all 50 million; many users pick the same eleven, so assume ~20 million distinct lineups (eleven players plus captain and vice-captain)
- Work per ball: 20 million lineups × 11 players ≈ 220 million multiply-adds, a fraction of a second spread across a small cluster
- Ranks per ball: ~100 million entries across about a million contests; the biggest contest has ~8 million entries
Conclusion: computing the points is cheap. The cost is in ranking 100 million entries and delivering the result to 15 million phones every 45 to 60 seconds.
Money#
- Entry debits: one per join: ~100 million ledger postings for a big match, ~22,000 a second in the window
- Prize credits: if 1 in 3 entries wins something (assumed), ~33 million credits after the match, all keyed so a retry can't pay twice
- Withdrawals: each one computes tax on net winnings for the financial year so far: an aggregate over the user's ledger, not a single row
Conclusion: the wallet's peak write rate is the join rate; its hard parts are idempotency, the tax rule, and proving the prize pool paid out exactly.
Key Metrics Summary#
| Metric | Value |
|---|---|
| Concurrent users at the peak (Dream11 CEO, IPL 2024) | 15.01 million |
| Requests at the peak (Dream11, IPL 2023) | 308 million a minute, ~5.1 million a second |
| Contest joins (Dream11, IPL 2020 / scaled to 2024) | 8,000 / ~23,000 a second |
| Design target for joins | ~70,000 a second |
| Teams / entries in a big match | ~50 million / ~100 million (assumed) |
| Biggest contest (Dream11, IPL 2020) | 7.9 million+ entries |
| Ball events | one every ~45 s |
| Leaderboard freshness (Dream11, 2017 SLA) | 60 s |
Requirements and estimation
Q1/3 Dream11 served 308 million requests a minute with 10.56 million concurrent users. Roughly how often does each open app send a request?
Answer & explanation
Answer: B
308 million ÷ 60 ≈ 5.1 million a second; ÷ 10.56 million users ≈ 0.5 a second each.
Q2/3 8,000 joins a second came with 5.3 million concurrent users in 2020. Scaled to 15 million concurrent, roughly how many?
Answer & explanation
Answer: B
8,000 × 15 ÷ 5.3 ≈ 23,000. A 3× design margin gives ~70,000.
Q3/3 Why is scoring 20 million lineups after every ball not the hard part?
Answer & explanation
Answer: B
Points are a dot product of 11 player scores and multipliers. Ranking 100 million entries and pushing results to 15 million phones is the real work.
Phase 3: High-Level Design (10 minutes)#
Sketch it first: Karthik's 7:29:58 edit
Karthik swaps his captain at 7:29:58 and joins the mega contest at 7:29:59. Which service checks the team is valid? What decides whether the edit is before the deadline, and whose clock does it use? Which component takes his spot in the mega contest without overselling it, and where does the entry fee come from? Then at 7:45 the first wicket falls: what turns it into a new rank on his phone?
One strong answer
- Team service validates the eleven (credits, per-side limit, captain and vice-captain) and writes a new version of the team, but only if the match is still open.
- The deadline is a state on the match record (
OPEN→LOCKED), flipped once by a lock service at the scheduled start. Every write checks it in the same atomic operation as the write, so no app server's clock decides. - Contest service takes one spot from the contest's spot counter (sharded for the mega contest) and the wallet debits the fee, tied together by one join id so a retry can't charge twice.
- At lock, every team is frozen into an immutable snapshot of distinct lineups.
- Live points: the score feed sends the wicket; the points engine updates player points, rescores lineups, and the rank engine rebuilds each contest's points histogram and top list. Karthik's phone reads his rank from those, cached per ball.
"Two halves with a hard line between them. Before the deadline: a write-heavy, spike-shaped system of teams, contests and a wallet, all checking one lock. After the deadline: a read-heavy, batch-shaped system that scores frozen teams ball by ball and serves ranks from precomputed summaries. The lock is the hand-off."
System Architecture#
%% title: A fantasy platform: writes until the lock, ranks after it
flowchart TD
subgraph L1["Layer 1 · Users and data"]
direction LR
APP@{ icon: "sd:phone", label: "Fantasy app<br/>teams, contests, ranks" }
FEED@{ icon: "sd:stream", label: "Score feed<br/>ball by ball" }
end
subgraph L2["Layer 2 · Edge"]
direction LR
CDN@{ icon: "sd:cdn", label: "CDN<br/>lineups, contest lists" }
GW@{ icon: "sd:gateway", label: "API gateway<br/>priority, shedding" }
end
subgraph L3["Layer 3 · Before the deadline"]
direction LR
TEAM["Team service<br/>validate, version"]:::service
CON["Contest service<br/>spot counters, templates"]:::hot
LOCK["Lock service<br/>match OPEN to LOCKED"]:::core
WAL["Wallet and ledger<br/>debit, credit, tax"]:::core
end
subgraph L4["Layer 4 · After the deadline"]
direction LR
PTS["Points engine<br/>players, lineups"]:::core
RANK@{ icon: "tech:spark", label: "Rank engine · Spark<br/>histograms, top lists" }
LB@{ icon: "tech:redis", label: "Redis<br/>ranks per ball" }
end
subgraph L5["Layer 5 · State"]
direction LR
KV@{ icon: "sd:database", label: "Team and contest store<br/>strongly consistent" }
BUS@{ icon: "tech:kafka", label: "Kafka<br/>joins, balls, points" }
end
APP ==> CDN
APP ==> GW
GW ==> TEAM
GW ==> CON
CON --> WAL
TEAM --> KV
CON --> KV
LOCK -.->|"lock at start"| KV
FEED ==> PTS
KV -->|"frozen lineups"| PTS
PTS ==> BUS ==> RANK ==> LB
GW --> LB
class APP client
class FEED external
class CDN,GW edge
class RANK service
class LB cache
class KV store
class BUS queueKEY INSIGHTS:
- The deadline divides the system. Before it, the hard problem is concurrent writes in a spike; after it, the data is frozen and the hard problem is recomputation and fan-out
- Reads never touch the write path. Lineups, contest lists and rules come from the CDN and caches; ranks come from per-ball summaries, never from a query over entries
- One lock, checked everywhere. Every team write and every join reads the match state in the same atomic operation that performs the write
Data Flow: Karthik's Last-Second Edit and Join#
%% title: Karthik edits his captain and joins the mega contest at 7:29 sequenceDiagram autonumber participant A as Karthik's app participant G as API gateway participant T as Team service participant C as Contest service participant W as Wallet participant S as Store A->>G: Save team 2, new captain, edit id e81 G->>T: Tier 1 request, admitted T->>S: Write version 5 if match OPEN S-->>T: Written at 7:29:58 A->>G: Join mega contest with team 2, join id j44 G->>C: Tier 1 request, admitted C->>C: Take a spot from shard 17 C->>W: Debit entry fee, key j44 W-->>C: Debited C->>S: Entry recorded if match OPEN S-->>A: Joined, spot held
MEANWHILE, IN PARALLEL:
- At 7:30:00 the lock service flips the match to
LOCKED; any write that reaches the store after that fails its check and the app shows "deadline passed" - A snapshot job then freezes every team into a table of distinct lineups for the points engine
High-level design
Q1/3 Karthik's request reaches an app server whose clock says 7:29:59 but the match was locked a moment ago. What decides?
Answer & explanation
Answer: C
One authoritative state flipped once, checked atomically with every write. No server's clock is trusted to decide.
Q2/3 Where does Karthik's phone get his rank from during the match?
Answer & explanation
Answer: B
Ranks are precomputed per ball and cached; a request is a few lookups, never a sort.
Q3/3 Why freeze every team into distinct lineups at the deadline?
Answer & explanation
Answer: B
Scoring ~20 million distinct lineups instead of ~50 million teams, from data that can no longer change.
Phase 4: Deep Dives (20 minutes)#
Deep Dive 1: The Toss-to-Deadline Spike (Week 10 Concepts: Capacity Planning; Week 4: Thundering Herd)#
"The worst thirty minutes are known days in advance: the fixture list says when, the match's popularity says how big, and the toss says go. That makes this a forecasting problem first and an autoscaling problem a distant second."
This builds on Week 10, Day 4 · Capacity Planning, Week 4, Day 3 · Thundering Herd and Week 3, Day 3 · Backpressure and Flow Control.
7:00 pm: the coins go up
At 7:00 pm, 15 million apps are open. At 7:01 the playing elevens are published and a push notification goes out: "Lineups are out!" Within seconds every app asks for the same two team sheets. Reactive autoscaling takes several minutes to add servers. What happens if capacity is added only when CPU rises? What happens to the lineup endpoint if the cached lineup expires at 7:01:00? And if, despite everything, joins and saves arrive faster than you can serve them, which requests should fail first?
One strong answer
- Reactive is too late. The surge arrives in a minute and lasts thirty; servers that take five minutes to boot arrive after the worst of it. Capacity must be in place before the toss, sized from a forecast.
- The lineup herd: millions of requests miss the same cache key at once and all go to the origin. Publish the lineup as a new immutable version (a new key) pushed to the CDN before the notification goes out, coalesce any misses into one origin fetch, and stagger the notification over a few seconds.
- Shed by priority: team saves and contest joins are tier 1 and must succeed; contest lists and team previews are tier 2 and can be served stale; leaderboards of other matches, social and recommendations are tier 3 and can be switched off. When tier 1 is at risk, reject tier 3 at the gateway first.
What is published: Dream11 wrote in 2021 that its traffic is "very spiky", that it runs more than 100 microservices and over 30,000 compute resources for peak IPL traffic, and that a data-science model (XGBoost over "over 200 variables") forecasts hourly concurrency by match tier. Its in-house platform, Scaler, provisions ahead of time: an AWS case study (2023) describes forecasts 5 days before a match, an updated forecast 2 hours before, and scaling that now starts 1 hour before (it used to take 3), across 700+ auto-scaling groups and 200 load balancers, with scaling time cut "from multiple hours down to about five minutes". In one case the updated forecast was "almost double what was projected earlier", about 10 million users. Dream11 also load-tests every service with its own tools (Torque, Playground) in "Game Days", and its contest service keeps 10 copies of the hottest cache keys so no single node serves them all.
- Forecast, then pre-scale in slabs. Each match gets a tier and a predicted peak; capacity is set per tier for before, during and after the match, scaled up an hour ahead and down after. Autoscaling remains as a safety net, not the plan.
- Stateful systems scale earlier. App servers boot in minutes; adding nodes to a database or cache cluster means rebalancing data, which must finish well before the toss.
- Edge traffic is the biggest. The AWS write-up notes about 245 million requests a minute at edge services against about 69 million at a non-edge service: most requests should end at the CDN or the gateway's caches.
- Make the herd small. Lineups, contest lists and player stats are versioned and pushed; clients add jitter to polling; a notification goes out in waves. The patterns are the ones in the Week 4 thundering herd lesson: request coalescing, early refresh and stale-while-revalidate.
- Admission by tier. The gateway tags each route with a tier and watches tier-1 latency; when it climbs, it rejects tier-3 requests with a "retry later" before they reach a service, then serves tier 2 from stale caches. A user who can't load someone else's profile is fine; a user whose team didn't save is not.
# gateway/shedding.py
"""
Priority load shedding at the gateway (illustrative). Each route has a tier; when
tier-1 latency rises toward its budget, lower tiers are rejected or served stale
first, so team saves and contest joins keep their capacity in the last minutes.
"""
TIER = {
"POST /teams": 1, "POST /contests/join": 1,
"GET /contests": 2, "GET /lineups": 2, "GET /teams/preview": 2,
"GET /leaderboard/other": 3, "GET /feed": 3, "GET /recommendations": 3,
}
BUDGET_MS = 300 # tier-1 p99 budget
def admit(route: str, tier1_p99_ms: float) -> str:
tier = TIER.get(route, 3)
pressure = tier1_p99_ms / BUDGET_MS
if tier == 1:
return "ADMIT"
if tier == 3 and pressure > 0.6:
return "REJECT_RETRY_LATER" # 503 with Retry-After and jitter
if tier == 2 and pressure > 0.8:
return "SERVE_STALE" # from cache, even if a version old
return "ADMIT"Deep Dive 2: Contest Joins and Spot Allocation (Week 1 Concepts: Hot Keys and Skew; Week 2: Idempotency)#
"A contest is a number that must go down by exactly one per join and never below zero. For a ten-spot contest that's trivial. For a mega contest with millions of spots taking thousands of joins a second, the number itself becomes the hottest key in the system."
This builds on Week 1, Day 4 · Hot Keys and Skew and Week 2, Day 2 · Idempotency in Practice.
Ten million spots, one counter
The mega contest has 10,000,000 spots. In the last five minutes it takes 15,000 joins a second. One key holding "spots left" can take maybe 20,000 atomic updates a second on one node. How do you take spots without overselling and without one node doing all the work? What happens near the end, when some parts of your scheme have spots and others don't? Meanwhile a ₹49 contest for 4 people fills every few milliseconds: how do you keep offering it? And a join's wallet debit times out: what makes the retry safe?
One strong answer
- Shard the spots. Split 10,000,000 spots into 100 shards of 100,000, each its own key on a different node. A join picks a random shard and decrements it only if it's above zero. No shard can go below zero, and the shards add up to exactly 10,000,000, so the contest can't be oversold.
- The tail: when the picked shard is empty, try two or three others; when the total left is small, a sweeper moves the remaining spots into a few shards. The contest fills exactly; at worst the last few joins take one or two extra tries.
- Templates for small contests: the ₹49, 4-spot contest is a template, not a single contest. Joins go to the current open copy; when it fills, the next copy is created already, so there is always one open. Thousands of copies fill independently, with no shared counter.
- One join id. The app generates a join id; the spot, the debit and the entry all carry it. A retried debit with the same key returns the first result; a spot taken by a join whose debit finally failed is given back.
What is published: Dream11 reported more than 7.9 million entries in a single contest and 8,000 contest joins a second in IPL 2020, and that it serves "millions of contests in a single match", generated per match by a contest-generation engine. Its contest service caches contests in Aerospike and spreads the hottest keys by writing "10 duplicate copies" of them. Its team service runs Aerospike in strong-consistency mode; Aerospike's webinar with Dream11 describes it at over a million transactions a second.
%% title: One join: idempotent, sharded, never oversold
flowchart TD
J["Join request<br/>join id, team, contest"]:::service
IDEM{"Seen this join id?"}
T{"Template or mega?"}
OPEN["Current open copy<br/>next copy pre-created"]:::service
SHARD["Spot shard, random<br/>take one if above 0"]:::hot
DEB@{ icon: "sd:wallet", label: "Wallet debit<br/>same join id" }
ENTRY@{ icon: "sd:database", label: "Entry row<br/>if match OPEN" }
BACK["Give the spot back<br/>debit failed"]:::bad
J ==> IDEM
IDEM -->|"yes"| ENTRY
IDEM ==>|"no"| T
T -->|"small"| OPEN ==> DEB
T ==>|"mega"| SHARD ==> DEB
DEB ==> ENTRY
DEB -.->|"declined"| BACK
class DEB store
class ENTRY store- Exactness comes from the shards, not from a total. Each shard is a conditional decrement on one key; the sum of shard allotments is the contest size. There is no moment when two shards can both hand out the last spot, because there is no shared last spot.
- Spread the shards across nodes, not just across keys: 100 shards on 100 partitions turn 15,000 joins a second into 150 per key.
- Order matters: spot, then money, then entry. A spot held without money is cheap to return; money taken without a spot is a refund and a support ticket. If the debit fails, a compensating step returns the spot to its shard.
- The entry write checks the lock. A join that took a spot and debited at 7:29:59.9 but writes its entry at 7:30:00.1 fails the match-state check; the spot and the fee go back. Exactly what the deadline promised.
- Unfilled contests follow their rules. A contest that doesn't fill by the deadline is either guaranteed (it runs, smaller) or cancelled and refunded by reversing postings: a decision made per contest at creation and recorded on it.
- Duplicates by template, not by copying load. The contest-generation engine decides which templates exist for a match; each template keeps one open copy and a pre-created next one, so a full copy is never a user-visible error.
# contests/spots.py
"""
Taking one spot in a large contest (illustrative). Spots are split across shards,
each a key that can only be decremented while positive; the allotments sum to the
contest size, so the contest cannot be oversold. A join id makes retries safe.
"""
import random
TRIES = 3
class ContestFull(Exception):
pass
def take_spot(kv, contest_id: str, shards: int, join_id: str) -> int:
held = kv.get(f"join:{join_id}")
if held is not None:
return held # a retry: same spot as before
for s in random.sample(range(shards), min(TRIES, shards)):
if kv.decrement_if_positive(f"spots:{contest_id}:{s}"):
kv.set(f"join:{join_id}", s, ttl_s=3600)
return s
left = kv.sum_prefix(f"spots:{contest_id}:") # rare: a near-full contest
if left == 0:
raise ContestFull(contest_id)
s = kv.rebalance_into_one(contest_id) # move the stragglers together
if kv.decrement_if_positive(f"spots:{contest_id}:{s}"):
kv.set(f"join:{join_id}", s, ttl_s=3600)
return s
raise ContestFull(contest_id)
def give_back(kv, contest_id: str, join_id: str) -> None:
s = kv.pop(f"join:{join_id}")
if s is not None:
kv.increment(f"spots:{contest_id}:{s}") # debit failed or deadline passedThe spike and the joins
Q1/4 Dream11 starts scaling an hour before a big match. Why not rely on autoscaling when CPU rises?
Answer & explanation
Answer: B
Forecast and pre-scale; keep autoscaling as a safety net.
Q2/4 The lineup is published and 15 million apps ask for it at once. What keeps the origin alive?
Answer & explanation
Answer: B
The thundering herd defences from Week 4: one fetch per miss, content ready before demand.
Q3/4 A mega contest has 10 million spots in 100 shards. Why can't it be oversold?
Answer & explanation
Answer: B
No shared "last spot" exists, so no two joins can both take it.
Q4/4 A join took a spot, but its wallet debit was declined. What happens?
Answer & explanation
Answer: C
Spot, then money, then entry; undo the spot if the money fails.
Deep Dive 3: Lock-Time Consistency (Week 5 Concepts: Consistency Models; Week 2: Distributed Cron)#
"The deadline is a promise to 25 million people that nobody saw the first ball before choosing. If one edit slips through after the start, everyone in that contest has been cheated. So 'after the deadline' must mean one thing, decided in one place."
This builds on Week 5, Day 1 · Consistency Models and Week 2, Day 5 · Distributed Cron and Job Scheduling.
7:29:59.950
Karthik's edit leaves his phone at 7:29:59.4 on a slow network and reaches the team service at 7:30:00.2 by its clock. Another server, whose clock runs 300 ms slow, receives a different edit at what it thinks is 7:29:59.9. The phone's own clock is 40 seconds fast. Which edits count? Then rain delays the start by 45 minutes after the deadline was announced. What must change, and what must not?
One strong answer
- Not a clock, a state. No phone, app server or database clock decides. At the scheduled start, a single lock job flips the match from
OPENtoLOCKEDwith a version bump in a strongly consistent store. Every team write and entry write is a conditional write: "only if this match's state isOPEN". Whatever commits before the flip counts; whatever arrives after fails, regardless of what any clock said. - Karthik's edit counts only if its write committed before the flip. The app shows the result it gets back, not what the phone's clock suggests.
- The phone's clock is used only for display: the countdown is computed from the server's deadline and the server time sent with each response.
- Rain: the deadline is data, not code. Operations moves the match's deadline to the new start; the lock job is rescheduled; users can keep editing. What must not change: a match that has already been locked is never reopened, because some users may have seen information after the lock.
What is published: Dream11's 2017 leaderboard team wrote that it maintained a consistent view across platforms with "snapshot isolation", using insert-only operations and immutable datasets. Its IPL 2023 post lists new team creation rules and an "auto-substitutes" feature among that season's product changes.
%% title: A match's life, from fixtures to settlement stateDiagram-v2 [*] --> OPEN: fixture published OPEN --> OPEN: rain, deadline moved later OPEN --> LOCKED: scheduled start, one flip LOCKED --> LIVE: first ball LIVE --> COMPLETED: last ball LIVE --> ABANDONED: no result COMPLETED --> VERIFIED: scorecard checked VERIFIED --> SETTLED: prizes credited ABANDONED --> REFUNDED: entries reversed SETTLED --> [*] REFUNDED --> [*] class SETTLED ok class ABANDONED bad class LOCKED queue
- Version every team. A team edit writes version n+1 with the match version it saw; the lock flip bumps the match version, so a write carrying an old version fails even if it was delayed in a queue.
- Freeze into a snapshot. Right after the flip, a job reads every team's last committed version and writes an immutable table: lineup hash → players, captain, vice-captain, and entry → lineup hash. The points engine reads only this snapshot; the live team tables can't change it.
- Lock is a fence, not a broadcast. The lock job doesn't need every server to hear about it; the store rejects late writes. Caches of "match is open" can be stale for a second without harm, because they only decide what the app shows, never what is accepted.
- Never reopen. Information arrives after the start (the first ball). Moving a deadline earlier or reopening a locked match would let some users edit with knowledge others didn't have. Delays only move an open deadline later.
- Non-playing players. If a picked player doesn't play, he scores zero; a product rule like auto-substitutes (swapping in a pre-chosen backup at lock time) must also be applied in the snapshot job, once, by a deterministic rule recorded with the snapshot.
- Abandoned matches go to
REFUNDED: every entry is reversed in the ledger by the same keyed postings used for joins.
# teams/save.py
"""
Saving a team edit (illustrative). The write is conditional on the match still
being OPEN at the version the client saw, checked atomically by the store, so no
server clock decides what counts as "before the deadline".
"""
class DeadlinePassed(Exception):
pass
def save_team(store, match_id: str, user_id: str, team_no: int, lineup: dict, edit_id: str):
validate(lineup) # 11 players, 100 credits, per-side limit, C and VC
m = store.get(f"match:{match_id}")
if m.state != "OPEN":
raise DeadlinePassed(match_id)
ok = store.transact([
store.check(f"match:{match_id}", state="OPEN", version=m.version),
store.put_if_new_edit(f"team:{match_id}:{user_id}:{team_no}", lineup, edit_id=edit_id),
])
if not ok:
raise DeadlinePassed(match_id) # the flip committed first
return {"saved": True, "server_time": store.now(), "deadline": m.deadline}
def lock_match(store, match_id: str):
m = store.get(f"match:{match_id}")
if m.state == "OPEN":
store.compare_and_set(f"match:{match_id}", expect_version=m.version, state="LOCKED")
enqueue("snapshot_lineups", match_id) # freeze teams into lineups, onceDeep Dive 4: Live Points, Ball by Ball (Week 8 Concepts: Streaming vs Batch; Late Data)#
"A ball is a small event with a large blast radius. A four changes one batter's points and the bowler's economy; that changes the score of every lineup containing either of them, which could be most of the 50 million teams; that changes ranks in every contest. The trick is to do the expensive part once per distinct thing: per player, per lineup, per contest."
This builds on Week 8, Day 2 · Streaming vs Batch Processing and Week 8, Day 4 · Late Data.
Over 14.3, a four, then a correction. The batter hits a four and the feed sends it; forty seconds later it sends a correction: a leg-bye, so the runs go to the team, not the batter. Karthik's vice-captain is that batter. Here is how the design handles both events:
- Players first. The points engine keeps each of the 22 players' points as a function of the full ball-by-ball record, not a running sum: it recomputes the batter's and bowler's points from their ball records.
- Lineups by inverted index. At lock, build player → lineups. Only lineups containing a changed player are rescored: lineup points = sum of its 11 players' points, ×2 for the captain, ×1.5 for the vice-captain. Karthik's lineup moves by 1.5 × the batter's change.
- Corrections are just new versions. Each ball event has an id and a version. The correction replaces ball 14.3's record; the batter's points are recomputed from the corrected record, and the lineups and ranks follow. Nothing is "subtracted", so applying it twice changes nothing.
- Untouched lineups keep their totals: on a typical ball, two players change, so lineups containing neither don't move.
What is published: Dream11's leaderboard post (2017) describes points for "composition of up to 11 players selected from the squad and points scored by the players for every action", calculated by a point calculation engine ("Aryabhata") on Apache Spark that recomputes team points, groups them by contest, sorts and applies competitive ranking, updating "every minute across thousands of contests". Dream11's published points system gives the captain 2× and the vice-captain 1.5× their points.
%% title: From one ball to new ranks, each step done once per distinct thing
flowchart TD
F@{ icon: "sd:stream", label: "Score feed<br/>ball 14.3, version 2" }
P["Player points<br/>recomputed from ball records"]:::core
IDX@{ icon: "sd:index", label: "Player to lineups<br/>built at lock" }
LU["Lineup points<br/>sum of 11, C 2x, VC 1.5x"]:::core
K@{ icon: "tech:kafka", label: "Kafka<br/>lineup deltas" }
R@{ icon: "tech:spark", label: "Rank engine · Spark<br/>histogram, top list" }
C@{ icon: "tech:redis", label: "Redis<br/>contest summaries, ball 14.3" }
F ==> P ==> LU
IDX -.-> LU
LU ==> K ==> R ==> C
class F external
class IDX store
class K queue
class R service
class C cache- Ball records are the source of truth. Store every delivery as a versioned record; player points are a pure function of a player's records and the scoring table. Replays, corrections and a crashed worker all produce the same numbers.
- Order by ball, not by arrival. The feed may deliver 14.4 before a correction to 14.3. Because points are recomputed from the full record set, order doesn't matter for correctness; a per-match sequence number lets the rank stage tag its output "as of ball 14.4, version n".
- Points are discrete. Runs, wickets and catches are whole points; the vice-captain's 1.5× makes halves. So every lineup score is a multiple of 0.5. This matters for ranking (Deep Dive 5).
- Micro-batch per ball. Rather than streaming each lineup update into ranking separately, the rank stage runs once per ball (or every few seconds when balls come quickly), over all lineup deltas since the last run: one Spark job per ball, not millions of tiny updates.
- Coalesce under pressure. If ranking falls behind (two balls in 40 seconds plus a correction), skip intermediate states and rank the latest; users only ever see the newest consistent "as of" ball.
# points/lineups.py
"""
Rescoring lineups after one ball (illustrative). Player points are recomputed from
the versioned ball records; only lineups containing a changed player are touched.
Scores are kept in half-points as integers, since the vice-captain multiplier is 1.5.
"""
MULT2 = {"C": 4, "VC": 3, "P": 2} # doubled multipliers: 2x, 1.5x, 1x
def player_points(records, scoring) -> dict:
pts = {}
for r in records: # every ball's latest version only
for player, action in r.actions():
pts[player] = pts.get(player, 0) + scoring[action]
return pts
def rescore(changed: dict, old: dict, by_player: dict, lineups: dict, totals: dict) -> dict:
"""changed/old: player -> points now / before. Returns lineup -> new total (half-points)."""
touched = set().union(*(by_player.get(p, ()) for p in changed))
for lid in touched:
role = lineups[lid].role_of # player -> "C", "VC" or "P"
totals[lid] += sum(MULT2[role(p)] * (changed[p] - old.get(p, 0))
for p in changed if role(p))
return {lid: totals[lid] for lid in touched}Deep Dive 5: Leaderboards at Scale: Approximate, Exact and Final (Week 1 Concepts: Hot Keys; Week 4: Feed Caching)#
"There are two leaderboards, really. One during the match, read by 15 million people every minute, which must be fast and believable. One after the match, read by the payout system once, which must be exact. They should not be the same computation."
This builds on Week 4, Day 4 · Feed Caching and Week 1, Day 4 · Hot Keys and Skew.
Rank 1,84,210 of 79,00,000
The mega contest has 7.9 million entries. After every ball, every entrant wants their rank, and the top 100 want to see the table. A sorted set holding all entries would take millions of updates per ball. How else can you tell Karthik his rank, exactly, in one lookup? How do you show the top 100? And at the end, 12 entries are tied on 412.5 points across ranks 1,000 to 1,011, which carry different prizes: what is each tied entry's rank and prize?
One strong answer
- A histogram, not a sort. Lineup scores are multiples of 0.5 in a range of roughly 0 to 1,500, so a contest's scores fit in ~3,000 buckets. After each ball, count entries per bucket (one pass over the contest's entries) and take a running sum from the top. Karthik's rank = 1 + the number of entries in buckets above his score: one lookup in a 12 KB array.
- The top 100: from the histogram, find the score above which 100 entries sit; select only entries at or above it in the same pass, sort that small set, and cache it.
- Ties share the rank. Competitive ranking: all 12 tied entries are rank 1,000, and the next entry is rank 1,012. Their prize is the sum of the prizes for ranks 1,000 to 1,011, divided equally among the 12.
What is published: Dream11's 2017 leaderboard was built to "crunch gigabytes of data using distributed sorting" within an SLA of under a minute: teams "grouped by the participating contest, sorted by total points and ranked as per competitive ranking", for "around a million contests" of 2 to 400,000 teams, 40 million records, served at a peak of 1 million requests a minute to 250,000 concurrent users from a 10-node Cassandra cluster with Redis caching at the web servers.
- Rank from a histogram is exact, as long as buckets are as fine as the scoring step. If a game's scores were continuous, coarser buckets would give an approximate rank ("about 1,84,000"), which is fine during a match but not for prizes.
- Approximate is acceptable while live. Showing a rank "as of ball 14.3" that is one ball behind is fine; showing a wrong rank after settlement is not. The live leaderboard can be a little stale; the final one must be recomputed from the verified scorecard.
- Read path: a few cached lookups. A rank request reads the user's entries (lineup ids), their lineup totals and the contest's histogram for the current ball version; all three are cached in Redis, with the hottest contests' summaries copied to several nodes, as Dream11 did for its hottest contest keys.
- Small contests are cheap. A 2-person contest doesn't need a histogram; sort its entries. The histogram path is for the few large contests where sorting millions per ball costs too much.
- Push the change, not the table. The app polls a tiny "latest ball version" endpoint from the CDN and fetches ranks only when it changes; 15 million phones never re-download a leaderboard every few seconds.
- The final leaderboard is a separate, exact job. After the match is verified, rank every contest from the final lineup totals with a full sort and competitive ranking, write the ranks and tie groups once, and hand them to settlement.
# ranks/histogram.py
"""
Exact live ranks from a points histogram (illustrative). Scores are integers in
half-points (the vice-captain earns 1.5x), so a contest fits in a few thousand
buckets; competitive ranking falls out of a running sum from the top.
"""
MAX_HALF_POINTS = 3000 # 1,500 points, in half-points
def build(totals_of_entries) -> list:
hist = [0] * (MAX_HALF_POINTS + 1)
for s in totals_of_entries:
hist[min(s, MAX_HALF_POINTS)] += 1
above = [0] * (MAX_HALF_POINTS + 2) # above[s] = entries with score > s
for s in range(MAX_HALF_POINTS, -1, -1):
above[s] = above[s + 1] + hist[s + 1] if s < MAX_HALF_POINTS else 0
return above
def rank(above: list, score: int) -> int:
return 1 + above[min(score, MAX_HALF_POINTS)] # ties share the rank (1, 2, 2, 4)
def tied_prize(prizes: list, first_rank: int, tied: int) -> float:
"""Split the prizes for ranks first_rank .. first_rank+tied-1 equally among the tied entries."""
window = prizes[first_rank - 1:first_rank - 1 + tied]
return sum(window) / tiedDeep Dive 6: Wallet, Ledger and Winnings (Week 2 Concepts: Idempotency; Week 3: Audit Logs)#
"The wallet has three kinds of money with different rules: what the user deposited, what they won, and promotional bonus. Tax law cares about the difference between the first two. A ledger that keeps them apart, and keys every posting, makes both settlement and tax a sum."
This builds on Week 2, Day 2 · Idempotency in Practice and Week 3, Day 5 · Audit Log System.
Karthik withdraws ₹3,000
In the financial year, Karthik deposited ₹1,000 and started with a zero balance. His teams won prizes; his balance is now ₹3,400. He withdraws ₹3,000. Section 194BA of the Income-tax Act requires 30% tax to be deducted from net winnings at withdrawal. Using the prescribed formula (withdrawals so far − deposits − opening balance − net winnings already taxed), how much tax is deducted, and how much reaches his bank? Then the settlement job crashes after crediting half of a contest's prizes and is restarted: what stops the first half being paid twice?
One strong answer
- Net winnings: ₹3,000 − (₹1,000 + ₹0 + ₹0) = ₹2,000. TDS at 30% = ₹600; ₹2,400 reaches his bank. ₹600 is posted to "TDS payable" and deposited with the government.
- Next withdrawal of ₹400: withdrawals so far ₹3,400 − (₹1,000 + ₹0 + ₹2,000 already taxed) = ₹400 net winnings → ₹120 TDS. At the end of the year the same formula runs once more including the closing balance.
- Settlement retry: each prize credit is a posting keyed by (contest, entry, settlement version). The restarted job replays the whole contest; the first half's keys already exist, so those postings are skipped. The contest's total credited must equal its prize pool before it is marked settled.
What is published: the Finance Act 2023 taxes net winnings from online games at 30% (Section 115BBJ) and requires the platform to deduct tax at source on net winnings at the time of withdrawal and at the end of the financial year (Section 194BA, from 1 July 2023), computed by the formula in Rule 133 of the Income-tax Rules; from 1 April 2026 the Income-tax Act, 2025 carries the same deduction under Section 393(3), at the same 30% with no threshold. Separately, from 1 October 2023 GST on online money gaming has been levied at 28% on the full amount deposited by the player, rather than 18% on the platform's fee; money re-entered from winnings is not treated as a fresh deposit.
%% title: Money in, entry fees, prizes and tax, each a balanced posting
flowchart TD
U@{ icon: "sd:card", label: "Deposit<br/>UPI or card" }
GST["GST payable<br/>28% of the deposit"]:::service
DEP@{ icon: "sd:wallet", label: "Deposit balance<br/>user's own money" }
POOL["Contest pool<br/>fees held until settled"]:::core
LED["Ledger<br/>append-only, keyed"]:::hot
WIN@{ icon: "sd:wallet", label: "Winnings balance<br/>prizes credited" }
TDS["TDS payable<br/>30% of net winnings"]:::service
BANK@{ icon: "sd:bank", label: "User's bank<br/>withdrawal" }
U ==> LED
LED --> GST
LED --> DEP
DEP ==>|"join"| POOL
POOL ==>|"settle"| WIN
WIN ==>|"withdraw"| BANK
WIN --> TDS
class U client
class DEP,WIN store
class BANK external- Three balances, one ledger. Deposits, winnings and bonus are separate accounts per user; a join debits them in a fixed order (a product rule, recorded on each posting), so the tax formula can always tell deposits from winnings.
- The contest pool is an account. Entry fees move into the contest's pool, minus the platform fee. Settlement moves the pool to winners. The pool must reach zero, and the sum of credits must equal the published prize table, before the contest is marked settled.
- Every posting is keyed. Joins by join id, prizes by (contest, entry, settlement version), refunds by (contest, entry, "refund"), withdrawals by withdrawal id. A replay or retry finds the key and does nothing.
- Corrections are new versions. If a verified scorecard is later corrected before settlement, the final ranks are recomputed and settlement runs with version 2; nothing posted for a version is ever edited. Once settled, a later change is a business decision handled by new postings, not a rewrite.
- Tax is a query, then a posting. At withdrawal, sum the user's withdrawals, deposits and already-taxed net winnings for the financial year from the ledger, apply the formula, and post the TDS in the same transaction as the withdrawal.
- Reconcile three ways every day: deposits against the payment gateway's settlement files, withdrawals against the bank's payout reports, and contest pools against prize tables. Anything unmatched is a ticket.
# wallet/withdraw.py
"""
A withdrawal with tax deducted at source on net winnings (illustrative). Net
winnings for the financial year follow the prescribed formula; the TDS, the user
debit and the payout instruction are posted together under one withdrawal key.
"""
from decimal import Decimal
TDS_RATE = Decimal("0.30")
def net_winnings(withdrawn_fy: Decimal, deposits_fy: Decimal, opening: Decimal, taxed_fy: Decimal) -> Decimal:
return max(Decimal(0), withdrawn_fy - (deposits_fy + opening + taxed_fy))
def withdraw(ledger, user: str, amount: Decimal, wid: str, fy: str):
if ledger.exists(key=f"wd:{wid}"):
return ledger.get(key=f"wd:{wid}") # a retried request
agg = ledger.fy_aggregates(user, fy) # sums of the user's postings this year
nw = net_winnings(agg.withdrawn + amount, agg.deposits, agg.opening, agg.taxed)
tds = (nw * TDS_RATE).quantize(Decimal("0.01"))
with ledger.tx():
ledger.post(key=f"wd:{wid}", entries=[
(f"user:{user}:winnings", -amount), # the user's balance goes down by the full amount
("tds_payable", tds), # owed to the government
("payouts_in_transit", amount - tds), # what reaches the bank
])
ledger.mark_taxed(user, fy, nw)
return {"paid": amount - tds, "tds": tds}Lock, points, ranks and money
Q1/6 A team edit reaches the store 200 ms after the match flipped to LOCKED, but the app server's clock says 7:29:59. What happens?
Answer & explanation
Answer: B
The store's state decides, atomically with the write. Clocks only drive the countdown on screen.
Q2/6 Rain delays a match by 45 minutes, before the lock. What may change?
Answer & explanation
Answer: B
Delays only extend an open deadline; reopening after the lock would let some users act on information others didn't have.
Q3/6 The feed corrects ball 14.3 from a four to a leg-bye. Why is applying the correction twice harmless?
Answer & explanation
Answer: B
Recomputing from versioned records is idempotent; subtracting deltas is not.
Q4/6 How can a rank in a 7.9-million-entry contest be exact in one lookup?
Answer & explanation
Answer: B
Discrete scores make an exact histogram cheap; the rank is 1 + the entries above.
Q5/6 Twelve entries tie for ranks 1,000 to 1,011. How are prizes paid?
Answer & explanation
Answer: C
Competitive ranking plus an equal split keeps the pool's total exact.
Q6/6 Karthik deposited ₹1,000 this year, started at zero and withdraws ₹3,000. How much TDS?
Answer & explanation
Answer: B
Net winnings = 3,000 − 1,000 = 2,000; 30% of that is ₹600.
Phase 5: Scaling and Edge Cases (5 minutes)#
"Walk me through Karthik's match night from the platform's side, then the cases that bite."
Karthik's Match Night#
- 5 days before · Forecast
- The concurrency model predicts the peak for this match's tier; contest templates for the match are generated
- 5:30 pm · Updated forecast, scale-up
- The forecast is refreshed 2 hours ahead; from 6:30 pm, app servers, caches and stores are scaled to the predicted slab and verified
- 7:00 pm · Toss, lineups out
- The lineup goes to the CDN as a new version, then the notification goes out in waves; joins climb past 20,000 a second
- 7:25 pm · Last five minutes
- The gateway sheds tier-3 routes and serves contest lists stale; team saves and joins stay under 300 ms
- 7:30:00 pm · Lock
- The match flips to LOCKED; late writes fail; the snapshot job freezes ~50 million teams into ~20 million lineups
- 7:30 to 10:50 pm · Live
- ~250 balls; each one is scored, ranked and visible within a minute
- Next morning · Settle
- The scorecard is verified, final ranks are computed with ties, prizes are credited by keyed postings, pools reconcile to zero
Edge Cases#
Edge case 1: The forecast is wrong
Problem: A rivalry match draws double the predicted concurrency, as Dream11 reported once for about 10 million users.
Solution: Re-forecast 2 hours before and scale to the new slab; keep headroom on stateful tiers; shed tier-3 traffic early; let autoscaling handle the stateless remainder.
Edge case 2: A player is announced, then withdrawn at the toss
Problem: Millions of teams include a player who won't play.
Solution: That's what the toss-to-deadline window is for; push a "not playing" flag with the lineup. After the lock, he scores zero; any substitute rule runs once in the snapshot job, deterministically.
Edge case 3: The feed stalls or sends a bad event
Problem: The data provider stops for ten minutes, or credits a wicket to the wrong bowler.
Solution: Show "updating" with the last ball version rather than guessing; take a second feed and compare; corrections are new versions of ball records, and settlement waits for the verified scorecard.
Edge case 4: The match is abandoned
Problem: Rain washes out the match after the lock.
Solution: The match goes to ABANDONED; every entry is reversed by keyed refund postings; pools return to zero; no ranks are settled.
Scaling and edge cases
Q1/3 The forecast for a match turns out to be half the real concurrency. What's the first line of defence?
Answer & explanation
Answer: B
Forecast, re-forecast, pre-scale; autoscaling handles only the stateless remainder.
Q2/3 The score feed stops for ten minutes mid-match. What do users see?
Answer & explanation
Answer: B
Never guess points. Show the last consistent state and say so.
Q3/3 A match is abandoned after the lock. What happens to the entries?
Answer & explanation
Answer: C
Refunds use the same keyed postings as joins, so a replayed refund can't pay twice.
Phase 6: Monitoring and Operations#
"Four questions on match night: is capacity ahead of demand, are tier-1 writes healthy, is the lock exact, and are ranks fresh? And the morning after: is the money exact?"
Monitoring Dashboard#
Before the deadline
| Metric | Now |
|---|---|
| Concurrent users / forecast | 14.2M / 15.0M |
| Requests | 4.6M/s |
| Contest joins | 26,400/s |
| Team saves | 37,900/s |
| Tier-1 p99 (save, join) | 182 ms |
| Tier-3 requests shed | 8% |
The lock and the money
- Writes rejected after lock (last match): 41,200; writes accepted after lock: 0
- Mega contest spots left: 312,880 across 100 shards; oversold contests: 0
- Join debits retried / duplicated: 1,940 / 0
Live
- Ball event to rank visible, p99: 38 s; feed lag: 4 s
- Distinct lineups: 19.6 million; rank stage per ball: 11 s
SLOs for a Match Night#
SLO 1: Tier-1 writes hold up in the spike
- Target: team save and contest join p99 < 300 ms and success > 99.95% from toss to deadline
SLO 2: The deadline is exact
- Target: zero writes accepted after the lock; lock flipped within 1 s of the scheduled start
SLO 3: Ranks are fresh
- Target: 99% of balls visible in every user's rank within 60 s
SLO 4: Money is exact
- Target: zero oversold contests; zero duplicate debits or prize credits; every contest pool reconciled to zero within 24 hours of verification
Reading the dashboard
Q1/3 "Writes accepted after lock" shows 3. What does that mean?
Answer & explanation
Answer: B
The target is zero. Any non-zero count means some write path doesn't check the match state atomically.
Q2/3 Ball-to-rank p99 climbs from 38 s to 3 minutes while the feed lag stays at 4 s. Where do you look?
Answer & explanation
Answer: A
The feed is fine; the backlog is in ranking. Skip intermediate balls and rank the latest.
Q3/3 Mega contest "spots left" stalls at 40 while joins keep failing as "full". Likeliest cause?
Answer & explanation
Answer: B
The tail problem of sharded counters: a few spots in many shards. Rebalancing fixes it.
Final Challenge: The Final, With Rain#
The interviewer leans back. "One more."
"It's the final. The forecast says 20 million concurrent. The toss happens on time, but rain starts at 7:20 and play is delayed. Ten minutes later the umpires say the match will be reduced to 15 overs a side, starting at 8:45. Meanwhile your feed provider is sending ball events every few seconds from another match that overlaps the delay. Tell me what happens to the deadline, the load, the scoring and the settlement."
- Forecast concurrency 20 million (assumption); joins scale to ~30,000 a second in the toss window
- Toss at 7:00 pm; rain at 7:20; at 7:30 the start is moved to 8:45, 15 overs a side
- The biggest contest: 10 million spots, 9.2 million filled at 7:20
- The feed: ~250 balls normally; ~190 in a 15-over game with extras
- Settlement runs after the verified scorecard; a scorer correction is expected the next morning
What do you change for the final?
Decide what happens to the deadline and the lock job, what the load does after 7:30, whether teams can still change, how scoring changes for 15 overs, and how settlement handles the next-morning correction.
One strong answer
- Deadline: at 7:20 the match is still
OPEN. Operations moves the deadline to 8:45 before 7:30; the lock job is rescheduled. Teams stay editable; nothing was locked, so nothing is reopened. Had the lock already flipped, the deadline could not move back. - Load: the spike now has two peaks: the original 7:25 rush (many users don't know about the delay yet) and a second rush before 8:45, possibly with a changed lineup. Keep capacity at the peak slab until 9:00; re-forecast for the new deadline.
- Joins: the mega contest fills its last 800,000 spots in the second window: 100 shards, a sweeper for the tail, templates keeping small contests open.
- Scoring: the points table is the same per action; a 15-over game simply has fewer balls (~190). Leaderboard freshness is unchanged. If the match is abandoned instead, every entry is refunded by keyed postings.
- Settlement: wait for the verified scorecard; settle version 1; if the scorer's correction arrives before settlement, rank again and settle version 2 instead. Every prize posting carries the version, so nothing is paid twice.
- Measure: writes after lock (zero), tier-1 p99 across both peaks, ball-to-rank p99, and pool reconciliation after settlement.
Final challenge · Quest check
Q1/3 Rain at 7:20, match still OPEN. What happens to the 7:30 deadline?
Answer & explanation
Answer: B
An open deadline can move later. A locked match is never reopened.
Q2/3 Why plan for two peaks on the final?
Answer & explanation
Answer: B
The second peak arrives after a delay; keep capacity at the peak slab until the new deadline passes.
Q3/3 A scorer's correction arrives the next morning, before settlement. What runs?
Answer & explanation
Answer: B
Settle from the verified scorecard; versioned keys make a re-run safe.
Interview Conclusion#
"Why not keep a sorted set per contest and update it as points change? It's the textbook leaderboard."
"For a 2-person contest, sure. For a contest with millions of entries, a single ball can change most of their scores, which means millions of sorted-set updates per ball for one contest. Because fantasy points come in half-point steps, a histogram of a few thousand counters gives the exact rank of any score in one lookup, and only the top of the table needs sorting. The sorted set solves a problem we don't have, where scores change one at a time."
"And why not just autoscale for the toss?"
"Because the toss is on the fixture list. The surge arrives within a minute and lasts thirty; a new server that takes several minutes arrives late, and a database that has to rebalance arrives later. Dream11 forecasts five days out, re-forecasts two hours out, and scales an hour ahead. Autoscaling is the safety net."
"The single most important decision?"
"Making the deadline a state change checked atomically with every write, not a time compared on a server. Everything after it, the snapshot, the lineups, the ranks, the payouts, depends on the teams being exactly the teams that existed at that moment."
Summary: Concepts Applied from 10-Week Course#
- Week 1: Data at scale
- Hot keys and skew — sharded spot counters for mega contests; hot contest summaries copied to several nodes
- Week 2: Failure-first design
- Idempotency — join ids, keyed prize credits and refunds, withdrawal keys
- Week 3: Messaging and async processing
- Backpressure — tiered admission and shedding at the gateway; an append-only, auditable ledger
- Week 4: Caching
- Thundering herd — versioned lineups pushed before the notification, coalesced misses; per-ball rank summaries cached
- Week 5: Consistency and coordination
- A match lock as one atomic state flip; conditional writes instead of clocks
- Week 8: Analytics pipelines
- Micro-batch scoring per ball; versioned ball records for late corrections
- Week 10: Production readiness
- Forecast-driven capacity planning; match-night SLOs
Why Dream11 Matters#
The peak is scheduled
- The worst thirty minutes are on the fixture list. The challenge is preparing for a known spike exactly, rather than reacting to an unknown one
Writes stop at a moment, then reads explode
- Every team must freeze at once; afterwards, one ball event fans out to tens of millions of scores and ranks
The regulatory ground moved
- Dream11 paused all paid contests in August 2025, as Parliament passed the Promotion and Regulation of Online Gaming Act, 2025 (assent 22 August 2025), which prohibits offering online money games, defined by stakes paid in the expectation of winnings, whether the game is one of skill or of chance. The Act and the Promotion and Regulation of Online Gaming Rules came into force on 1 May 2026. Challenges to the Act were transferred to the Supreme Court in September 2025; on 5 August 2026 the Court agreed to give the petitions a final hearing, and they are pending as this is written. Dream11 now runs free-to-play contests; the spike, the lock and the leaderboards are the same, and the wallet design applies wherever prize contests are lawful
"Decide the deadline in one place, score each distinct thing once, and rank by counting."
Self-Assessment Checklist#
After studying this case study, you should be able to:
The spike and the joins:
- Explain why a scheduled spike is handled by forecasting and pre-scaling, not reactive autoscaling
- Design tiered admission and load shedding that protects team saves and joins
- Take spots from a multi-million-spot contest with sharded counters, without overselling
The lock and live points:
- Make a deadline a single state change checked atomically with every write
- Freeze teams into distinct lineups and rescore only those containing a changed player
- Apply feed corrections idempotently from versioned ball records
Leaderboards and money:
- Compute exact ranks from a points histogram, and split prizes among tied entries
- Separate live (fresh, slightly stale) and final (exact, verified) leaderboards
- Model deposits, winnings, pools and tax as keyed ledger postings, and compute TDS on net winnings
Sources#
Dream11 and Dream Sports:
- Blog
Dream11 Engineering — IPL Fever: How Dream11 serves 10.56 million+ users during the TATA IPL 2023 (10.56 million concurrent at the opener; 308 million requests a minute, "about five million requests per second"; microservices for authentication, user, team, contest and leaderboard; auto-substitutes), 2 May 2023: https://tech.dream11.in/blog/ipl-fever-how-dream11-serves-10-56-million-users-during-the-tata-ipl-2023
- Blog
Qamar Ali Shaikh and Ritesh Sharma — To Scale In Or Scale Out? Here's How We Scale at Dream11 (100 million+ users; 5.5 million+ concurrency; 100 million+ requests a minute at edge services; 30,000+ compute resources; 100+ microservices; XGBoost concurrency forecast over 200+ variables; Scaler; Game Days), Dream11 Engineering, 2 April 2021: https://tech.dream11.in/blog/2021-04-02_To-Scale-In-Or-Scale-Out--Here-s-How-We-Scale-at-Dream11-f88ef5e71cbc
- Blog
Srijan Gupta and Anuj Gupta — How does Dream11 serve millions of Personalised Contests in a match (surge in contest joins after the toss; ~5 million concurrency post-toss; contest generation engine; VoltDB, Spark Streaming, Aerospike; 10 duplicate copies of hot keys), Dream11 Engineering, 12 January 2021: https://tech.dream11.in/blog/2021-01-12_How-does-Dream11-serve-millions-of-Personalised-Contests-in-a-match-cbb00463b7da
- Blog
Amit Mirchandani, Amit Sharma and Bharat Vishwakarma — Leaderboard @Dream11 (points per action for up to 11 players; Aryabhata point engine on Apache Spark; competitive ranking; leaderboards updated every minute; around a million contests of 2 to 400,000 teams; 40 million records within a 60-second SLA; 1 million requests a minute at 250,000 concurrent users; Cassandra; Redis; snapshot isolation with insert-only, immutable datasets), Dream11 Engineering, 24 October 2017: https://tech.dream11.in/blog/2017-10-24_Leaderboard--Dream11-4efc6f93c23e
- Blog
Sanket Raut, Vatsal Shah, Ritesh Sharma and Eeshan Bembi — How Dream11 uses an in-house scaling platform (forecast 5 days ahead, updated 2 hours before, scaling from 1 hour before instead of 3; a forecast once almost doubled to ~10 million; 700+ auto-scaling groups and 200 load balancers; ~245 million requests a minute at edge services vs ~69 million at a non-edge service; scaling in about five minutes; 100+ sporting events a day), AWS for Games Blog, 20 September 2023: https://aws.amazon.com/blogs/gametech/how-dream11-uses-an-in-house-scaling-platform
- Read
Aerospike — How Dream11 built a fantasy sports team service to handle futuristic IPL scale (strong-consistency mode; over a million transactions a second), webinar: https://aerospike.com/resources/webinars/how-dream11-built-a-fantasy-sports-team-service-to-handle-futuristic-ipl-scale/
Reporting:
- Read
Sportskeeda — Dream11 Points System (T20: captain 2× and vice-captain 1.5× points; 100 credits per team): https://www.sportskeeda.com/cricket/dream11-points-system
- Book
Entrepreneur India — Dream11 Reports 5.3 Mn-plus Concurrent Users During Ongoing IPL Season (5.3 million+ concurrent on 14 October 2020; 60 million requests a minute; 8,000+ contest joins a second; 18 million+ contestants in a single match; 7.9 million+ entries in a single contest; comments by CIO Abhishek Ravi), 28 October 2020: https://india.entrepreneur.com/news-and-trends/dream11-reports-53-mn-plus-concurrent-users-during-ongoing/358644
- Book
Inc42 — Dream11 Shines On IPL Debut, Onboards 1.1 Mn New Users (15.01 million concurrent users on the first day of IPL 2024, posted by CEO Harsh Jain on X), 22 March 2024: https://inc42.com/buzz/dream11-shines-on-ipl-debut-onboards-1-1-mn-new-users/
- Read
TechCrunch — As India bans real-money games, Dream Sports, MPL start pulling the plug (Parliament's passage on 20 and 21 August 2025; operators suspend real-money games), 21 August 2025: https://techcrunch.com/2025/08/21/as-india-bans-real-money-games-dream-sports-mpl-start-pulling-the-plug
- Wiki
Wikipedia — Dream11 (200 million users in October 2023; paid contests, over 90% of revenue, paused in August 2025; free-to-play since): https://en.wikipedia.org/wiki/Dream11
Law and regulation (India):
- Wiki
Promotion and Regulation of Online Gaming Act, 2025 (passed 20 and 21 August 2025; assent 22 August 2025; prohibits offering and advertising online money games): https://en.wikipedia.org/wiki/Promotion_and_Regulation_of_Online_Gaming_Act,_2025
- Read
MediaNama — Online Gaming Rules Notified, In Effect From May 1, 2026 (rules notified 22 April 2026; in force 1 May 2026; Online Gaming Authority under MeitY), April 2026: https://www.medianama.com/2026/04/223-online-gaming-rules-notified-may-1-major-changes/
- Read
Supreme Court Observer — Challenge to the Promotion and Regulation of Online Gaming Act, 2025 (High Court challenges transferred to the Supreme Court on 8 September 2025; pending): https://www.scobserver.in/cases/challenge-to-the-promotion-and-regulation-of-online-gaming-act-2025/
- Read
Kashmir Observer (PTI) — SC agrees to hear pleas challenging new online gaming law (final hearing agreed on 5 August 2026), 5 August 2026: https://kashmirobserver.net/2026/08/05/sc-agrees-to-hear-pleas-challenging-new-online-gaming-law/
- Blog
Taxmann — 28% GST shall be levied on online gaming and casinos w.e.f. 1.10.2023 (CBIC notifications of 29 September 2023; 28% on the amount deposited): https://taxmann.com/post/blog/28-gst-shall-be-levied-on-online-gaming-and-casinos-notification
- Book
Mondaq — Net Winnings From Online Games: Calculation Prescribed By CBDT (Rule 133 formulas for net winnings at withdrawal and at year end; Section 194BA from 1 July 2023), 19 April 2024: https://www.mondaq.com/india/income-tax/1452742/net-winnings-from-online-games-calculation-prescribed-by-cbdt
- Blog
TDSMAN — TDS on winnings from lottery, games, etc. (Section 194BA carried into Section 393(3) of the Income-tax Act, 2025 from 1 April 2026), May 2026: https://blog.tdsman.com/2026/05/tds-on-winnings-from-lottery-games-etc-section-3933-section-194b/
Design note: the architecture, code, thresholds, dashboards and every number labelled as an assumption or estimate are an illustrative design for the problem, not a description of Dream11's internal systems beyond what Dream11 has published. Users per match, teams per user, entries per team, the share of joins after the toss, distinct lineups, the final's figures and the tax example are assumptions. Tax and regulatory facts are summarised for engineering context, are not legal or tax advice, and should be checked against current law.
Further Reading#
Related systems to study:
- Read
IRCTC Tatkal (Bonus Problem 11 in this series): a scheduled spike at a known time, with admission control
- Read
BookMyShow (Bonus Problem 16): seats that must never be sold twice, under a flash crowd
- Read
Razorpay (Bonus Problem 18): payments, settlement and reconciliation from the provider's side
- Read
Zerodha Kite (Bonus Problem 13): a market-open spike at 9:15, and exactly-once orders
Course material:
- Talk
The course's Hotstar case file covers the other half of an IPL night: streaming the match itself to 25 million viewers
Foundational concepts (this course):
- Read
Week 1, Day 4 — Hot Keys and Skew
- Read
Week 2, Day 2 — Idempotency in Practice
- Read
Week 2, Day 5 — Distributed Cron and Job Scheduling
- Read
Week 3, Day 3 — Backpressure and Flow Control
- Read
Week 3, Day 5 — Audit Log System
- Read
Week 4, Day 3 — Thundering Herd
- Read
Week 4, Day 4 — Feed Caching
- Read
Week 5, Day 1 — Consistency Models
- Read
Week 8, Day 2 — Streaming vs Batch Processing
- Read
Week 8, Day 4 — Late Data
- Read
Week 10, Day 4 — Capacity Planning
"Decide the deadline in one place, score each distinct thing once, and rank by counting."
- 0/7stages cleared
New lessons by email
New lessons, side quests and case files as they ship, at most once a week. No spam, unsubscribe anytime.
Discussion · …
Public: everyone reading this lesson sees comments. Be kind and keep it on the material.