~learn.

Dream11 Fantasy Sports, the whole design.

A fantasy platform at IPL scale, by forecasting the toss-to-deadline spike and pre-scaling for it, taking contest spots from sharded counters under one join id, freezing every team with a single match-lock flip that every write checks, and ranking tens of millions of entries after every ball from points histograms.

7 diagrams · 8 sections · revise in ~10 min · Download PDF prints it in the light theme, diagrams included

SQ-25 · Dream11 Fantasy Sports · design sheethimanshukukreja.in/learn/sheets/bonus-25-dream11-fantasy-sports

At a glance#

Number Value What it drives
Peak concurrency 15.01M users, IPL 2024 opener capacity is forecast and added an hour before, not autoscaled
Requests 308M a minute · ~5.1M / s (IPL 2023) ~0.5 requests a second per open app; almost all are reads
Contest joins 8,000 / s in 2020 · ~23,000 / s scaled to 15M design for ~70,000 / s with tier-1 priority at the gateway
Biggest contest 7.9M+ entries (IPL 2020) one "spots left" key is too hot: shard it
A big match ~50M teams · ~100M entries (assumed) freeze into ~20M distinct lineups at the lock (assumed)
Toss to deadline ~30 minutes · ~40% of entries (assumed) ~22,000 joins / s average in the window
Ball events ~250 a T20 · one every ~45 s one rank pass per ball, visible within 60 s
Leaderboard SLA 40M records in 60 s (2017) ranks from per-ball histograms, never a sort per request

Architecture#

DiagramWrites until the lock, ranks after it
%% title: Writes until the lock, ranks after it
flowchart TD
  APP@{ icon: "sd:phone", label: "Fantasy app<br/>teams · contests · ranks" } ==> GW@{ icon: "sd:gateway", label: "API gateway<br/>tiers · shedding" }
  APP ==> CDN@{ icon: "sd:cdn", label: "CDN<br/>lineups · contest lists" }
  GW ==> TEAM["Team service<br/>validate · version"]:::service
  GW ==> CON["Contest service<br/>spot shards · templates"]:::hot
  CON --> WAL["Wallet and ledger<br/>keyed postings · TDS"]:::core
  TEAM --> KV@{ icon: "sd:database", label: "Team and contest store<br/>conditional writes" }
  CON --> KV
  LOCK["Match lock<br/>OPEN to LOCKED, once"]:::core -.-> KV
  FEED@{ icon: "sd:stream", label: "Score feed<br/>ball by ball" } ==> PTS["Points engine<br/>players · lineups"]:::core
  KV -->|"frozen lineups"| PTS
  PTS ==> BUS@{ icon: "tech:kafka", label: "Kafka<br/>lineup deltas" }
  BUS ==> RANK@{ icon: "tech:spark", label: "Rank engine · Spark<br/>histograms · top lists" }
  RANK ==> LB@{ icon: "tech:redis", label: "Redis<br/>contest summaries per ball" }
  class APP client
  class GW,CDN edge
  class KV store
  class FEED external
  class BUS queue
  class RANK service
  class LB cache
DiagramOne join: idempotent, sharded, never oversold
%% title: One join: idempotent, sharded, never oversold
flowchart TD
  J["Join request<br/>join id · team · contest"]:::service
  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/>only if match OPEN" }
  BACK["Give the spot back"]:::bad
  J ==> T
  T -->|"small"| OPEN ==> DEB
  T ==>|"mega"| SHARD ==> DEB
  DEB ==> ENTRY
  DEB -.->|"declined"| BACK
  class DEB,ENTRY store

Key flows#

DiagramKarthik edits his captain and joins the mega contest at 7:29
%% 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
  G->>T: Tier 1, admitted
  T->>S: Write version 5 if match OPEN
  S-->>T: Written at 7:29:58
  A->>G: Join mega contest, join id
  G->>C: Tier 1, admitted
  C->>C: Take a spot from shard 17
  C->>W: Debit entry fee, same join id
  W-->>C: Debited
  C->>S: Entry recorded if match OPEN
  S-->>A: Joined
  Note over S: at 7:30:00 the match flips to LOCKED · later writes fail
DiagramOne ball, from the feed to every rank
%% title: One ball, from the feed to every rank
sequenceDiagram
  autonumber
  participant F as Score feed
  participant P as Points engine
  participant R as Rank engine
  participant C as Rank cache
  participant A as Karthik's app
  F->>P: Ball 14.3, version 1, a four
  P->>P: Recompute batter and bowler from ball records
  P->>R: Deltas for lineups holding either player
  R->>R: Rebuild contest histograms and top lists
  R->>C: Summaries as of ball 14.3
  A->>C: Latest ball version changed?
  C-->>A: Rank 1,84,210, 412.5 points
  F->>P: Ball 14.3, version 2, leg-bye
  Note over P,R: recompute, never subtract · ranks follow the newest version
DiagramSettlement and a withdrawal, each a keyed posting
%% title: Settlement and a withdrawal, each a keyed posting
sequenceDiagram
  autonumber
  participant V as Verified scorecard
  participant R as Final ranks
  participant L as Ledger
  participant W as Withdrawal
  participant B as User's bank
  V->>R: Full sort, competitive ranking, tie groups
  R->>L: Prize credits keyed by contest, entry, version
  Note over L: pool must reach zero · credits equal the prize table
  W->>L: Sum this year's withdrawals, deposits, taxed winnings
  L-->>W: Net winnings 2,000 rupees
  W->>L: Debit 3,000 · TDS 600 · payout 2,400, one key
  W->>B: Send 2,400 rupees

Data model#

DiagramCore entities
%% title: Core entities
classDiagram
  class Match {
    +matchId : string
    +state : OPEN / LOCKED / LIVE / SETTLED
    +deadline : scheduled start
    +version : int
  }
  class Team {
    +userId : string
    +teamNo : int
    +players : 11 ids
    +captain : 2x
    +viceCaptain : 1.5x
    +version : int
  }
  class Lineup {
    +lineupHash : string
    +players : 11 ids, C, VC
    +points : half-points
  }
  class Contest {
    +contestId : string
    +template : string
    +spots : int
    +shards : int
  }
  class Entry {
    +joinId : string
    +contestId : string
    +lineupHash : string
  }
  class Posting {
    +key : idempotency key
    +entries : balanced, sum zero
  }
  Match "1" --> "many" Team : frozen at lock
  Team "many" --> "1" Lineup : snapshot
  Contest "1" --> "many" Entry : spots
  Entry "many" --> "1" Lineup : scored as
  Entry "1" --> "many" Posting : fee, prize, refund

Lifecycle#

DiagramA match, from fixture to settlement
%% title: A match, from fixture 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

Decisions#

Decision Chose Over Because
Capacity for the toss Forecast 5 days out, re-forecast 2 h before, scale from 1 h before Reactive autoscaling the surge arrives within a minute of the toss; stateful tiers need time to rebalance
Overload Tiered shedding: saves and joins first, tier 3 rejected, tier 2 stale Equal treatment of all routes a lost team save is a broken promise; a missing profile isn't
Mega contest spots 100 shards, each decremented only while positive One "spots left" counter ~15,000 joins / s on one key; shards can't oversell because their allotments sum to the size
Small contests Templates with an open copy and a pre-created next One contest that shows "full" a 4-spot contest fills in milliseconds
The deadline One OPEN to LOCKED flip, checked in every conditional write Comparing timestamps on servers clocks drift and requests are delayed; the store's state is the only truth
Scoring Recompute players from versioned ball records, rescore touched lineups Running deltas per team corrections and replays stay idempotent; work is per distinct lineup
Live ranks Half-point histograms per contest A sorted set per contest one ball changes millions of scores; a histogram rank is one lookup
Money Keyed, append-only postings; TDS computed from yearly aggregates Balance columns updated in place retries, settlement re-runs and tax must all be sums over the ledger

When it breaks#

Failure What users see Contain it with
Forecast far too low slow saves and joins near the deadline re-forecast 2 h out; shed tier 3 early; stale tier 2
Lineup notification herd lineup page fails at 7:01 versioned lineup on the CDN before the push; coalesced misses; waves
Spots left scattered across shards "contest full" with spots left try a few shards; sweeper rebalances the tail
A write path skips the lock check edits accepted after the start conditional writes everywhere; alert on any write after lock
Feed stalls or sends a wrong event ranks stop or jump show last ball version as updating; versioned records; settle from the verified card
Rank stage falls behind ranks minutes old coalesce to the latest ball; add workers
Settlement crashes halfway some winners credited keyed prize postings; re-run the whole contest; pool must reach zero

Say it in the interview#