system-design51 min read

How to Design Fantasy Sports Platform like Dream11 — A Senior+ Guide | Ayodhyya

How to Design Fantasy Sports Platform like Dream11 — A Senior+ Guide

Building contests, real-time scoring, leaderboards, and payout systems at 200M+ user scale

By Ayodhyya | Published: July 14, 2026 | System Design | 25 min read

1. Introduction — Fantasy Sports at 200M+ User Scale

Fantasy sports have exploded into one of the fastest-growing segments in the technology and gaming industry. Platforms like Dream11 (valued at over $8 billion), Mobile Premier League (MPL), My11Circle, FanDuel, and DraftKings have collectively attracted over 200 million registered users worldwide. In India alone, the fantasy sports market is projected to exceed $25 billion by 2030, driven by cricket, football, basketball, and kabaddi.

At its core, a fantasy sports platform allows users to create virtual teams composed of real-world athletes. These teams earn points based on the actual on-field performance of the selected players during live matches. Users compete in contests against other participants, and the highest-scoring teams win prize money. It is a skill-based game — not gambling — as team performance depends on statistical analysis, player knowledge, and strategic decision-making.

Building a fantasy sports platform that serves 200 million+ users with real-time score updates during a live IPL match — where millions of users simultaneously check their rankings — is a monumental systems design challenge. The platform must handle:

  • Massive concurrent traffic: 5-10 million users during peak match hours
  • Real-time data ingestion: Live ball-by-ball player statistics from multiple sports data providers
  • Sub-second score computation: Recalculating millions of fantasy teams as live scores change
  • Leaderboard updates: Millions of ranking changes per second during a live match
  • Financial transactions: Secure deposit, withdrawal, and prize distribution at massive scale
  • Fraud prevention: Detecting collusion, multi-accounting, and suspicious betting patterns
Why System Design Matters Here: A fantasy sports platform is one of the most complex distributed systems to design because it combines real-time event processing (live match data), heavy computation (scoring millions of teams), financial systems (payouts), and social features (leaderboards, chat) — all under extreme traffic spikes during popular matches.

In this comprehensive guide, we will design a fantasy sports platform from scratch — covering every major subsystem from data ingestion and scoring engines to database sharding, legal compliance, and cost estimation. We will include 8+ architecture diagrams, 5+ C# code implementations, detailed data models, API designs, and a 300+ line production-grade C# implementation that ties everything together.

Whether you are preparing for a senior software engineer interview at a FAANG company, building your own fantasy sports startup, or simply curious about how platforms like Dream11 handle IPL match-day traffic — this guide will give you a thorough, production-oriented understanding of the entire system.

Fantasy sports is not just a product — it is a real-time distributed computing problem disguised as a game. Every live match is a global event with millions of state transitions per second.

2. Functional & Non-Functional Requirements

Functional Requirements

FeatureDescriptionPriority
User Registration & AuthSign up via email, phone, social OAuth. JWT-based session management.P0
Match ListingDisplay upcoming, live, and completed matches across sports.P0
Contest BrowserList contests by entry fee, prize pool, spots, sport, and match.P0
Team CreationSelect 11 players, assign captain (2x) and vice-captain (1.5x), set roles.P0
Contest JoinJoin contests using wallet balance. Multiple team entries per contest.P0
Real-Time ScoringLive fantasy points updated as match progresses, ball-by-ball.P0
LeaderboardLive ranking within each contest, updated in real-time.P0
Prize DistributionAutomated payout after match completion. Wallet + bank transfer.P0
Wallet SystemDeposit, withdraw, bonus balance, transaction history.P0
Player StatsHistorical player performance, form, and selection percentage.P1
Social FeaturesInvite friends, share teams, public profiles.P1
NotificationsPush notifications for match reminders, contest updates, payouts.P1
Draft ModeSnake draft and auction draft for private leagues.P2
Free ContestsPractice contests with no entry fee, no real money.P1
Multi-SportSupport cricket, football, basketball, kabaddi.P1

Non-Functional Requirements

AttributeTargetStrategy
Availability99.99% uptime (52 min/year downtime)Multi-AZ, active-active, circuit breakers
Latency< 200ms API, < 500ms score update propagationEdge caching, Redis, CDN
Throughput50K+ QPS reads, 10K+ QPS writes during peakHorizontal scaling, DB sharding, CQRS
ConsistencyEventual for scores, Strong for financialsEvent sourcing for wallet, eventual for leaderboards
Scalability200M+ registered, 20M+ MAU, 5M+ DAU peakMicroservices, auto-scaling, sharding
SecurityPCI-DSS for payments, encryption at rest/transitVault, TLS 1.3, WAF, rate limiting
ComplianceSkill gaming regulations (India state-wise), KYC/AMLGeo-fencing, automated compliance checks
Fault ToleranceNo single point of failureChaos engineering, bulkheads, retries with backoff

3. Capacity Estimation

User & Traffic Estimation

MetricEstimationCalculation
Registered Users200 millionGiven
Monthly Active Users (MAU)80 million40% of registered
Daily Active Users (DAU)20 million25% of MAU
Peak Concurrent Users5 millionIPL match day peak, ~25% of DAU
Peak QPS (Reads)50,0005M users / 100 avg requests per session
Peak QPS (Writes)12,000Team creation, contest joins, wallet ops
Score Update Events/min500,0001M live teams × 1 ball event per 30s
Daily Contests50,000Across all sports and matches
Daily Transactions10 millionDeposits, withdrawals, contest joins

Storage Estimation

EntityRecord SizeAnnual RecordsAnnual Storage
Users1 KB50M new50 GB
Teams Created2 KB5 billion10 TB
Contest Entries0.5 KB10 billion5 TB
Score Events0.2 KB500 billion100 TB
Transactions0.5 KB3.6 billion1.8 TB
Total (compressed, TTL-managed)~35-50 TB (hot+warm+cold)

Bandwidth Estimation

Inbound: Sports data feeds at ~10 MB/s per active match × 10 concurrent matches = ~100 MB/s

Outbound: 50K QPS × 5 KB avg response = ~250 MB/s peak API traffic

Internal: Score event bus at ~200 MB/s during live matches (Kafka throughput)

4. Data Model

Entity Relationship Diagram

erDiagram USERS ||--o{ USER_TEAMS : creates USERS ||--o{ TRANSACTIONS : has USERS ||--o{ CONTEST_ENTRIES : joins MATCHES ||--o{ CONTESTS : has MATCHES ||--o{ PLAYER_PERFORMANCES : tracks MATCHES ||--o{ USER_TEAMS : targets CONTESTS ||--o{ CONTEST_ENTRIES : contains CONTESTS ||--o{ PRIZE_DISTRIBUTION : defines USER_TEAMS ||--o{ TEAM_PLAYERS : includes PLAYERS ||--o{ TEAM_PLAYERS : belongs_to PLAYERS ||--o{ PLAYER_PERFORMANCES : performs SPORTS ||--o{ MATCHES : governs SPORTS ||--o{ PLAYERS : includes

Core Tables

TableKey ColumnsShard KeyStorage
usersuser_id, email, phone, name, kyc_status, wallet_balance, created_atuser_idPostgreSQL
matchesmatch_id, sport_id, team_a, team_b, start_time, status, venuematch_idPostgreSQL
playersplayer_id, name, sport_id, team_name, role, image_url, is_activesport_idPostgreSQL
contestscontest_id, match_id, type, entry_fee, total_spots, filled_spots, prize_pool, statusmatch_idPostgreSQL + Redis
user_teamsteam_id, user_id, match_id, contest_id, captain_id, vice_captain_id, total_pointsmatch_idPostgreSQL + Redis
team_playersid, team_id, player_id, is_captain, is_vice_captainteam_idPostgreSQL
player_performancesid, match_id, player_id, points_breakdown, total_points, ball_by_ballmatch_idClickHouse + Redis
transactionstx_id, user_id, type, amount, status, balance_after, reference_iduser_idPostgreSQL
prize_distributionid, contest_id, rank, user_id, team_id, prize_amount, statuscontest_idPostgreSQL
sportssport_id, name, scoring_rules_json, min_team_size, max_team_sizeN/A (small table)PostgreSQL + Cache

Key Indexes

CREATE INDEX idx_contests_match_status ON contests(match_id, status);
CREATE INDEX idx_teams_match_contest ON user_teams(match_id, contest_id);
CREATE INDEX idx_teams_user ON user_teams(user_id, created_at DESC);
CREATE INDEX idx_tx_user_type ON transactions(user_id, type, created_at DESC);
CREATE INDEX idx_perf_match_player ON player_performances(match_id, player_id);

5. API Design

RESTful API Endpoints

MethodEndpointDescriptionAuth
POST/api/v1/auth/registerUser registrationNo
POST/api/v1/auth/loginLogin, returns JWTNo
GET/api/v1/matches?sport=cricket&status=liveList matchesYes
GET/api/v1/matches/{id}/playersPlayers for a matchYes
GET/api/v1/matches/{id}/contestsContests for a matchYes
POST/api/v1/contests/{id}/joinJoin contest with teamYes
POST/api/v1/teamsCreate teamYes
GET/api/v1/contests/{id}/leaderboardLive leaderboardYes
GET/api/v1/users/me/teams?match_id=XMy teams for a matchYes
POST/api/v1/wallet/depositDeposit fundsYes
POST/api/v1/wallet/withdrawWithdraw fundsYes
GET/api/v1/matches/{id}/scores/realtimeWebSocket upgrade for live scoresYes
DELETE/api/v1/teams/{id}Delete team (before lock time)Yes

WebSocket Protocol for Live Scores

Connection: wss://api.platform.com/ws/scores?match_id={id}&token={jwt}

Server Events:

  • score_update — Player score changed with delta
  • contest_rank_change — User's rank in contest changed
  • match_status — Match started, in progress, completed
  • leaderboard_snapshot — Full leaderboard refresh (every 60s)

Client Messages:

  • subscribe_contest — Subscribe to specific contest updates
  • ping — Keep-alive heartbeat every 30s

6. High-Level Architecture

graph TB subgraph Clients A[Mobile App iOS/Android] B[Web App React] C[Admin Panel] end subgraph Edge Layer D[CloudFront CDN] E[AWS WAF] F[API Gateway / Load Balancer] end subgraph Core Services G[Auth Service] H[Match Service] I[Contest Service] J[Team Service] K[Scoring Engine] L[Leaderboard Service] M[Wallet Service] N[Notification Service] O[User Profile Service] end subgraph Data Ingestion P[Sports Data Provider API] Q[Score Ingestion Worker] R[Apache Kafka] end subgraph Data Layer S[(PostgreSQL Cluster)] T[(Redis Cluster)] U[(ClickHouse Analytics)] V[S3 + Glacier] end subgraph External W[Payment Gateway] X[SMS/Email Provider] Y[KYC Service] end A --> D B --> D C --> D D --> E E --> F F --> G F --> H F --> I F --> J F --> L F --> M F --> O J --> K K --> L P --> Q Q --> R R --> K R --> L G --> S H --> S I --> S J --> S M --> S K --> T L --> T K --> U M --> W N --> X G --> Y

Service Responsibilities

API Gateway: Rate limiting (10K req/user/hour), JWT validation, request routing, SSL termination, CORS handling. Uses AWS ALB + Kong or Envoy proxy.

Auth Service: OAuth 2.0 + JWT, OTP verification, session management, role-based access control. Stateless, horizontally scalable.

Match Service: CRUD for matches, schedules, player availability. Consumes data from sports providers. Publishes match events.

Contest Service: Contest creation, join/leave logic, lock management, spot allocation. Uses distributed locks to prevent overfilling.

Team Service: Team CRUD, player selection validation, captain assignment, salary cap enforcement.

Scoring Engine: Real-time fantasy point computation. Ingests live match events, applies scoring rules, updates team scores via Redis.

Leaderboard Service: Maintains contest rankings using Redis sorted sets. Pushes rank updates via WebSocket.

Wallet Service: ACID-compliant financial transactions. Deposit, withdrawal, bonus management, prize crediting. Uses event sourcing.

7. Contest Lifecycle & State Machine

stateDiagram-v2 [*] --> CREATED : Admin creates CREATED --> OPEN : Contest visible OPEN --> FILLING : Users joining FILLING --> FULL : All spots filled FILLING --> LOCKED : Lock time reached FULL --> LOCKED : Lock time reached LOCKED --> IN_PROGRESS : Match started IN_PROGRESS --> COMPLETED : Match ended COMPLETED --> SETTLED : Prize distributed SETTLED --> ARCHIVED : Cleanup OPEN --> CANCELLED : Match cancelled FILLING --> CANCELLED : Match cancelled FULL --> CANCELLED : Match cancelled CANCELLED --> REFUNDED : Refunds processed REFUNDED --> ARCHIVED : Cleanup

Contest Types

TypeDescriptionMax EntriesExample
Head-to-Head2 users compete directly2Rs 49 entry, Rs 90 prize
Small League2-20 participants3-20Rs 35 entry, top 3 win
Grand LeagueThousands to millionsUnlimitedRs 49 entry, Rs 1Cr prize pool
Free ContestNo entry fee, practice modeVariesRs 0 entry, Rs 0 prize
Private LeagueInvite-only with friends2-50Rs 100 entry, split among top
Buyer's LeagueMultiple entries allowedPer user limit (e.g., 20)Max 20 teams per user

Lock Time Logic

Critical Concept: Contest lock time is typically set to the match start time. After lock time:
- No new teams can be created for this contest
- Existing teams cannot be edited
- Team data is frozen for scoring
- The system takes a snapshot of all teams for the scoring engine

Implementation: A scheduled job checks every 30 seconds for contests approaching lock time and triggers the snapshot + state transition.
sequenceDiagram participant Scheduler participant ContestService participant Redis participant Kafka participant ScoringEngine Scheduler->>ContestService: Check contests near lock time ContestService->>Redis: Lock contest (SETNX) ContestService->>Redis: Snapshot all teams ContestService->>Kafka: Publish ContestLocked event Kafka->>ScoringEngine: Initialize scoring for contest ScoringEngine->>Redis: Load all teams + points cache Note right of ScoringEngine: Ready for live scoring

8. Team Creation & Captain Selection

Team creation is one of the most user-intensive flows in the platform. Users must select exactly 11 players from a pool of 22-25 available players for a match, while adhering to role constraints and a virtual salary cap.

Team Validation Rules (Cricket Example)

RoleMinMaxDescription
Wicket Keeper (WK)13Must have at least 1 WK
Batsman (BAT)36Specialist batsmen
All-Rounder (AR)14Can bat and bowl
Bowler (BOWL)36Must have at least 3 bowlers
Total Players1111Exactly 11 required
Max per Real Team-7No more than 7 from one real team
Salary Cap-100 creditsTotal player credits <= 100

Captain & Vice-Captain Selection

Captain: Earns 2x fantasy points
Vice-Captain: Earns 1.5x fantasy points

These multipliers make captain selection the single most impactful decision in fantasy sports. Users can change their captain/vice-captain until contest lock time.
graph LR A[Select 11 Players] --> B{Validation Pass?} B -->|Yes| C[Assign Captain 2x] B -->|No| D[Show Error] C --> E[Assign Vice-Captain 1.5x] E --> F{Unique Selection?} F -->|Yes| G[Save Team] F -->|No| H[Error: C and VC different] G --> I[Ready for Contest Join] D --> A H --> E

9. Real-Time Player Scoring Engine

The scoring engine is the heart of the fantasy sports platform. It must ingest live match data from sports data providers, compute fantasy points for every player, and propagate score changes to millions of user teams within seconds.

Cricket Scoring Rules

EventPointsEventPoints
Run scored (per run)+1Wicket taken+25
Boundary (4)+4Catch+8
Six+6Stumping+12
Maiden over+12Run out+12
30-run bonus+45-wicket haul bonus+16
50-run bonus+810-wicket haul bonus+32
100-run bonus+16Economy rate bonusVaries
Strike rate bonusVariesDot ball+1
Wide ball-1No ball-1
Wicket duck (batsman)-2Caught out (bowler bonus)+4

Scoring Pipeline Architecture

graph LR A[Sports Data API] -->|Raw Events| B[Kafka: raw.match.events] B --> C[Event Parser] C -->|Parsed Events| D[Kafka: parsed.match.events] D --> E[Score Calculator] E -->|Player Points| F[(Redis: player_scores)] E -->|Events| G[Kafka: score.updates] G --> H[Team Score Aggregator] H -->|Updated Scores| I[(Redis: team_scores)] H -->|Leaderboard Events| J[Kafka: rank.updates] J --> K[Leaderboard Service] K -->|WebSocket| L[Mobile Clients]

Score Aggregation Flow

Key Challenge: When a batsman hits a six, the scoring engine must:
1. Update that player's fantasy points (+6)
2. Find ALL teams containing that player
3. Update every team's total score
4. Re-rank all teams in every affected contest
5. Push rank change notifications to users

This must happen in <500ms for 10+ million teams across thousands of contests. The solution: Redis sorted sets + event-driven aggregation.

Score Update Pseudocode

// When a scoring event arrives (e.g., player scores a six)
async Task ProcessScoringEvent(MatchEvent evt)
{
    // Step 1: Calculate fantasy points for this event
    int fantasyPoints = ScoringRuleEngine.Calculate(evt);

    // Step 2: Update player's total points in Redis
    string playerKey = $"player:{evt.MatchId}:{evt.PlayerId}";
    await Redis.StringIncrementAsync(playerKey, fantasyPoints);

    // Step 3: Get all teams containing this player
    var affectedTeams = await Redis.SetMembersAsync(
        $"player_teams:{evt.MatchId}:{evt.PlayerId}");

    // Step 4: Update each team's score
    foreach (var teamId in affectedTeams)
    {
        string teamKey = $"team_score:{evt.MatchId}:{teamId}";
        await Redis.StringIncrementAsync(teamKey, fantasyPoints);

        // Step 5: Update leaderboard sorted sets for all contests
        var contestIds = await Redis.SetMembersAsync(
            $"team_contests:{teamId}");
        foreach (var contestId in contestIds)
        {
            double newScore = await Redis.StringGetAsync(teamKey);
            await Redis.SortedSetAddAsync(
                $"leaderboard:{contestId}",
                teamId, newScore);
        }
    }

    // Step 6: Publish real-time update for WebSocket consumers
    await Kafka.PublishAsync("score.updates", new ScoreUpdate
    {
        MatchId = evt.MatchId,
        PlayerId = evt.PlayerId,
        PointsDelta = fantasyPoints,
        AffectedTeamCount = affectedTeams.Length
    });
}

10. Leaderboard & Rankings

Leaderboards are the most read-heavy component of a fantasy sports platform. During a live IPL match, millions of users continuously check their ranking. The leaderboard must update in near real-time as scores change.

Redis Sorted Set Implementation

graph TB subgraph Redis Cluster A[Sorted Set: leaderboard:contest_123] B[Sorted Set: leaderboard:contest_456] C[Sorted Set: leaderboard:contest_789] end subgraph Score Pipeline D[Score Event] --> E[Update Player Points] E --> F[Update Team Score] F --> G[ZRANK + ZADD] G --> H[Push to WebSocket] end G --> A G --> B G --> C

Leaderboard C# Service

// Redis Sorted Set operations for leaderboard
public class LeaderboardService
{
    private readonly IConnectionMultiplexer _redis;

    public async Task UpdateScoreAsync(
        string contestId, string teamId, double newScore)
    {
        var db = _redis.GetDatabase();
        string key = $"leaderboard:{contestId}";

        // ZADD: Add or update score
        await db.SortedSetAddAsync(key, teamId, newScore);

        // Get current rank (0-indexed)
        long rank = await db.SortedSetRankAsync(
            key, teamId, Order.Descending);

        // Publish rank change
        await PublishRankChange(contestId, teamId, rank + 1);
    }

    public async Task<List<LeaderboardEntry>> GetTopNAsync(
        string contestId, int count = 100)
    {
        var db = _redis.GetDatabase();
        string key = $"leaderboard:{contestId}";

        var entries = await db.SortedSetRangeByRankWithScoresAsync(
            key, 0, count - 1, Order.Descending);

        return entries.Select((e, i) => new LeaderboardEntry
        {
            Rank = i + 1,
            TeamId = e.Element,
            Score = e.Score
        }).ToList();
    }

    public async Task<int> GetUserRankAsync(
        string contestId, string teamId)
    {
        var db = _redis.GetDatabase();
        string key = $"leaderboard:{contestId}";

        long? rank = await db.SortedSetRankAsync(
            key, teamId, Order.Descending);

        return rank.HasValue ? (int)rank.Value + 1 : -1;
    }

    public async Task<List<LeaderboardEntry>> GetNearbyAsync(
        string contestId, string teamId, int range = 20)
    {
        var db = _redis.GetDatabase();
        string key = $"leaderboard:{contestId}";

        long? rank = await db.SortedSetRankAsync(
            key, teamId, Order.Descending);
        if (!rank.HasValue) return new List<LeaderboardEntry>();

        long start = Math.Max(0, rank.Value - range);
        long end = rank.Value + range;

        var entries = await db.SortedSetRangeByRankWithScoresAsync(
            key, start, end, Order.Descending);

        return entries.Select((e, i) => new LeaderboardEntry
        {
            Rank = (int)(start + i + 1),
            TeamId = e.Element,
            Score = e.Score,
            IsCurrentUser = e.Element == teamId
        }).ToList();
    }
}
Memory Optimization: For a contest with 1 million entries, a Redis sorted set uses approximately 50-80 MB. With 50,000 daily contests, we use ~2-4 TB of Redis memory. We achieve this through:
- TTL-based eviction (contest data expires 24h after match ends)
- Sharded Redis clusters (1024 hash slots)
- Compressed team IDs (int32 instead of UUID strings)

11. Prize Distribution & Payout System

The payout system handles real money and must be absolutely correct. A single bug in prize distribution can lead to massive financial loss and regulatory penalties.

Prize Distribution Models

ModelDescriptionUsed For
Winner Takes All1st place gets entire prize poolHead-to-head contests
Top N FlatEqual split among top N finishersSmall leagues (3-10 spots)
Percentage-BasedFixed percentage per rank positionGrand leagues
Diminishing CurveExponentially decreasing prizesLarge contests (100K+ entries)

Payout Processing Pipeline

graph TB A[Match Completed] --> B[Finalize Scores] B --> C[Lock Leaderboard] C --> D[Calculate Prize Distribution] D --> E[Create Payout Records] E --> F[Deduct Platform Commission 10-15%] F --> G[Credit Winnings to Wallet] G --> H[Update Transaction Ledger] H --> I[Send Notification] I --> J[Allow Withdrawal]
Financial Integrity: The payout system must use double-entry bookkeeping. Every transaction has a debit and credit entry. The sum of all debits must always equal the sum of all credits. The system must be idempotent — retrying a payout must not double-credit users.

Wallet Balance Breakdown

public class Wallet
{
    public decimal UnlockedBalance { get; set; }
    public decimal BonusBalance { get; set; }
    public decimal WinningsBalance { get; set; }
    public decimal ContestsPending { get; set; }

    public decimal TotalBalance => UnlockedBalance
        + BonusBalance + WinningsBalance;

    // Deposit: Goes to UnlockedBalance
    // Contest Join: Moves from UnlockedBalance to ContestsPending
    // Contest Win: Moves from ContestsPending to WinningsBalance
    // Contest Loss: Moves from ContestsPending to BonusBalance
    // Withdrawal: Deducts from WinningsBalance + UnlockedBalance
}

Withdrawal Flow

sequenceDiagram participant User participant WalletService participant FraudCheck participant KYCService participant PaymentGateway participant Bank User->>WalletService: Request withdrawal Rs 5000 WalletService->>FraudCheck: Validate withdrawal FraudCheck->>FraudCheck: Check velocity, patterns alt Fraud Detected FraudCheck-->>WalletService: Block + Alert WalletService-->>User: Withdrawal blocked end WalletService->>KYCService: Verify KYC status alt KYC Not Verified WalletService-->>User: Complete KYC first end WalletService->>WalletService: Lock funds (idempotent) WalletService->>PaymentGateway: Initiate transfer PaymentGateway->>Bank: NEFT/IMPS/UPI Bank-->>PaymentGateway: Transfer status PaymentGateway-->>WalletService: Confirmed WalletService->>WalletService: Debit wallet WalletService-->>User: Withdrawal processed

12. Match Schedule & Data Feeds

Fantasy sports platforms rely on external sports data providers for match schedules, player lists, and live ball-by-ball data. The platform must be resilient to provider outages and support multiple providers for redundancy.

Sports Data Providers

ProviderCoverageLatencyPricing
SportradarGlobal, all major sports< 200msEnterprise
Opta (Stats Perform)Football, Cricket, Tennis< 300msEnterprise
ESPN APIUS Sports, Cricket< 500msModerate
Cricbuzz/CricAPICricket only< 400msAffordable
Custom ScrapingSpecific leaguesVariableEngineering cost
graph TB subgraph Data Providers A[Sportradar API] B[Opta Feed] C[CricAPI] end subgraph Ingestion Layer D[Provider Adapter A] E[Provider Adapter B] F[Provider Adapter C] G[Normalization Engine] end subgraph Processing H[Kafka: raw.sports.data] I[Event Deduplication] J[Quality Validation] end subgraph Storage K[(PostgreSQL: Match Data)] L[(Redis: Live State)] M[Kafka: match.events] end A --> D B --> E C --> F D --> G E --> G F --> G G --> H H --> I I --> J J --> K J --> L J --> M
Redundancy Strategy: Use primary + secondary data providers. If primary fails or has latency > 500ms, automatically switch to secondary. Implement circuit breaker pattern with 30-second cooldown. All raw events are stored for audit trail and dispute resolution.

13. Draft System

While most Indian fantasy platforms use a "pick any player" model, international platforms like FanDuel and Yahoo Fantasy offer draft-based systems for season-long leagues.

Snake Draft

graph LR A[Round 1: User A picks] --> B[Round 1: User B picks] B --> C[Round 1: User C picks] C --> D[Round 2: User C picks] D --> E[Round 2: User B picks] E --> F[Round 2: User A picks] F --> G[Round 3: User A picks] style A fill:#0088ff,color:#fff style D fill:#0088ff,color:#fff style G fill:#0088ff,color:#fff

In a snake draft, users take turns picking players in a serpentine order. Once a player is picked, no other user can select them. The order reverses each round, creating a balanced drafting experience.

Auction Draft

In an auction draft, each user gets a virtual budget (e.g., $200). Players are nominated one at a time, and users bid on them. The highest bidder gets the player. This continues until all roster spots are filled.

FeatureSnake DraftAuction Draft
Duration15-30 minutes1-3 hours
Skill TypePreparation + luckStrategy + valuation
FairnessLottery determines orderEqual budgets for all
ConcurrencySequential picks (turn-based)Real-time bidding
Best ForCasual leaguesCompetitive leagues

14. Anti-Fraud & Collusion Detection

Fantasy sports platforms are targets for various forms of fraud. Detecting and preventing these is critical for platform integrity and regulatory compliance.

Fraud Types & Detection

Fraud TypeDescriptionDetection MethodAction
Multi-AccountingOne person using multiple accounts to gain advantageDevice fingerprinting, IP analysis, behavioral biometricsAccount ban, contest disqualification
CollusionMultiple accounts coordinated to split prizesGraph analysis, entry pattern correlation, timing analysisContest investigation, prize withholding
Insider InformationUsing non-public team lineup info before publicTeam creation timing vs lineup announcementMonitoring alerts, manual review
Chip DumpingLosing deliberately to transfer fundsAbnormal loss patterns, velocity analysisAccount restriction
Bot UsageAutomated team creation with optimal lineupsAPI rate analysis, timing patterns, user-agent analysisRate limiting, account review
graph TB A[User Action Stream] --> B[Feature Extraction] B --> C[Risk Scoring Engine] C --> D{Score > Threshold?} D -->|Low Risk| E[Allow] D -->|Medium Risk| F[Flag for Review] D -->|High Risk| G[Auto Block] F --> H[Manual Investigation] H --> I{Confirmed Fraud?} I -->|Yes| J[Ban Account] I -->|No| K[Remove Flag] J --> L[Blacklist Device + IP] G --> L
Key Signals for Collusion Detection:
  • Multiple accounts always joining the same contests
  • Accounts created around the same time with similar patterns
  • Suspiciously complementary team compositions (no overlapping players)
  • Funds transferred between accounts before contest entry
  • Same device or IP accessing multiple accounts
  • Consistent profit-sharing patterns across contests

15. User Engagement

User engagement and retention are critical for fantasy sports platforms. The key metrics are D1, D7, D30 retention, and the "sticky" factor of users returning for each match.

Engagement Features

FeatureDescriptionImpact
Push NotificationsMatch reminders, lineup announced, contest fill alerts, payout credits+40% D1 retention
StreaksDaily login rewards, consecutive contest participation bonuses+25% D7 retention
Social SharingShare team on social media, challenge friends+30% organic acquisition
AchievementsBadges for milestones (first win, 100 contests, high score)+15% engagement
Refer & EarnBonus credits for referring friends who deposit+50% new user acquisition
Live CommentaryIn-app ball-by-ball commentary with score context+35% session time
Expert PicksCurated team suggestions from cricket experts+20% contest joins for new users

Notification Architecture

graph LR A[Event Trigger] --> B[Notification Router] B --> C{User Preferences} C -->|Push Enabled| D[FCM / APNs] C -->|Email Enabled| E[SES / SendGrid] C -->|SMS Enabled| F[Twilio / MSG91] C -->|In-App| G[WebSocket Push] D --> H[Mobile Device] E --> I[Email Inbox] F --> J[SMS] G --> K[Web Dashboard]

16. Free-to-Play & Practice Contests

Free-to-play contests serve multiple purposes: they onboard new users, help users learn the platform without financial risk, and maintain engagement during low-stakes periods. Many platforms also use free contests as a regulatory strategy in regions with strict gambling laws.

Business Value: Free contests act as a conversion funnel. Data shows that users who play 5+ free contests have a 35% higher likelihood of joining paid contests. Free contests also serve users in Indian states where real-money fantasy sports are restricted (Andhra Pradesh, Telangana, Assam, Odisha, Nagaland).

Implementation Considerations

  • Separate scoring pipeline: Free contests can use a simplified scoring pipeline with lower priority
  • Virtual currency: Some platforms give "practice coins" that have no real-world value
  • Geo-fencing: Automatically route users in restricted states to free-only mode
  • Leaderboard separation: Free and paid contest leaderboards are kept separate
  • Anti-abuse: Prevent users from creating multiple accounts to exploit free contest prizes

17. Multi-Sport Support

A production fantasy sports platform must support multiple sports to maintain year-round engagement. Cricket has season gaps (no international cricket in some months), so football, basketball, and kabaddi fill the engagement void.

Sport Comparison

AttributeCricketFootballBasketballKabaddi
Team Size111187
Match Duration3-8 hours90 min48 min40 min
Scoring Events/Min~5-10~2-4~15-25~8-12
Data ComplexityVery HighMediumHighMedium
Peak MarketIPL, World CupPremier League, FIFANBA SeasonPKL
SeasonYear-roundAug-MayOct-JunJul-Oct
Architecture Approach: Each sport is a configuration, not a separate codebase. The scoring rules, team composition constraints, and player roles are stored as JSON configuration. The scoring engine reads these configs at runtime to handle any sport. This makes adding a new sport a configuration change, not a code deployment.

18. Database Sharding

At 200M+ users and billions of team entries, a single PostgreSQL database cannot handle the load. We need a sharding strategy that distributes data across multiple database nodes.

Sharding Strategy

graph TB subgraph Application Layer A[Contest Service] --> B[Shard Router] C[Team Service] --> B D[Wallet Service] --> E[Shard Router Wallet] end subgraph Shard Router - by match_id B --> F{match_id mod 16} F -->|0-3| G[Shard 0-3: Matches A-D] F -->|4-7| H[Shard 4-7: Matches E-H] F -->|8-11| I[Shard 8-11: Matches I-L] F -->|12-15| J[Shard 12-15: Matches M-P] end subgraph Shard Router Wallet - by user_id E --> K{user_id mod 8} K -->|0-1| L[Wallet Shard 0-1] K -->|2-3| M[Wallet Shard 2-3] K -->|4-5| N[Wallet Shard 4-5] K -->|6-7| O[Wallet Shard 6-7] end
Data TypeShard KeyShard CountRationale
Matches, Contestsmatch_id16 shardsAll data for a match co-located
User Teamsmatch_id16 shardsTeams on same shard as contest
Wallet / Transactionsuser_id8 shardsACID per user, prevents double-spend
User Profileuser_id8 shardsUser data always co-located
Player Performancesmatch_id16 shardsHigh-write, co-located with match
Hot Shard Problem: A mega match (India vs Pakistan in World Cup) might attract 10 million contest entries, making its shard extremely hot. Mitigations: (1) Pre-partition large matches across sub-shards, (2) Use read replicas for query-heavy operations, (3) Cache aggressive leaderboards in Redis.

19. Caching Strategy

Multi-Layer Cache Architecture

graph TB A[Client Request] --> B[L1: CDN Cache] B -->|Miss| C[L2: Application Cache - Local] C -->|Miss| D[L3: Redis Cluster] D -->|Miss| E[L4: PostgreSQL] E --> D D --> C C --> B subgraph Cache Layers F[TTL: Static assets 24h] G[TTL: Match list 30s] H[TTL: Leaderboard 1s] I[TTL: Player stats 5min] end
DataCache LayerTTLInvalidation
Static assets (images, CSS)CDN (CloudFront)24 hoursVersioned URLs
Match listingRedis + CDN30 secondsWrite-through on status change
Contest detailsRedis5 secondsEvent-driven invalidation
LeaderboardRedis Sorted SetReal-time (no TTL)Updated on every score event
Player scores (live)RedisReal-timeUpdated on every scoring event
Player stats (historical)Redis + CDN5 minutesRefresh after each match
User sessionRedis24 hoursJWT expiration
Scoring rulesLocal in-memoryUntil config changeConfig service push
Cache Stampede Prevention: During live matches, leaderboard cache invalidation happens thousands of times per second. We use singleflight pattern — if 1000 requests simultaneously try to refresh the same leaderboard, only one actually executes the DB query, and the other 999 wait for the result. Redis Sorted Sets avoid this entirely since they are updated incrementally.

21. Multi-Region Design

For a platform serving users across India and potentially globally, multi-region deployment ensures low latency and regulatory compliance.

graph TB subgraph Global Edge A[CloudFront Global] B[Route 53 Latency-Based] end subgraph Mumbai Region - Primary C[API Cluster] D[PostgreSQL Primary] E[Redis Primary] F[Kafka Cluster] end subgraph Delhi Region - Secondary G[API Cluster] H[PostgreSQL Replica] I[Redis Replica] J[Kafka MirrorMaker] end subgraph Singapore Region - International K[API Cluster] L[PostgreSQL Replica] M[Redis Replica] end A --> B B -->|Indian Users| C B -->|North India Users| G B -->|International Users| K D --> H D --> L E --> I E --> M F --> J
Data Residency: User PII and financial data must remain in India per DPDP Act. Match data and scoring data can be replicated internationally for serving international users. The scoring engine runs in the primary region with event replay capability in secondary regions.

22. Cost Estimation

ComponentSpecificationMonthly Cost (USD)
API Servers (ECS/EKS)20 x c5.2xlarge (peak), 10 x c5.xlarge (off-peak)$15,000
PostgreSQL (RDS)db.r5.4xlarge x 4 (primary) + 8 read replicas$12,000
Redis Cluster (ElastiCache)6 x r5.2xlarge nodes (192 GB total)$6,500
Apache Kafka (MSK)6 x kafka.m5.2xlarge brokers$4,500
ClickHouse Analytics4-node cluster on EC2$3,000
S3 + Glacier100 TB S3, 500 TB Glacier archival$3,500
CloudFront CDN50 TB/month transfer$4,000
ALB + WAFApplication Load Balancer + WAF rules$2,000
Notification ServicesPush + SMS + Email (100M/month)$5,000
Sports Data FeedsSportradar + Opta enterprise licenses$25,000
Payment Gateway Fees~2% of transaction volume$50,000
Monitoring & ObservabilityDatadog / Grafana Cloud$3,000
Security & ComplianceVault, WAF, audit tools, PCI compliance$5,000
DevOps & CI/CDGitHub Actions, container registry$2,000
Total Monthly~$140,500
Annual (with 30% buffer)~$2.2M
Cost Optimization Strategies:
  • Spot Instances: 60-70% savings on scoring engine workers (fault-tolerant batch processing)
  • Reserved Instances: 1-year commitments for databases and Redis (30-40% savings)
  • Auto-scaling: Scale down to 30% capacity during non-peak hours (11 PM - 6 AM IST)
  • Tiered storage: Move completed match data to Glacier after 30 days
  • Redis TTL: Automatic eviction of contest data 24h post-match

23. Interview Q&A (12 Questions)

Q1: How would you handle 10 million concurrent users during an India vs Pakistan World Cup match?
Answer: I would implement a multi-layer approach: (1) CDN for all static content, (2) edge caching for match listings and contest data, (3) horizontal auto-scaling of API servers behind ALB, (4) Redis for all read-heavy operations (leaderboards, scores, contest info), (5) database read replicas for query offloading, and (6) WebSocket connections via dedicated connection servers with sticky sessions. The key insight is that 80% of read traffic can be served from Redis without touching the database.
Q2: How do you ensure real-time score propagation to millions of teams within 500ms?
Answer: The scoring pipeline uses Kafka for event streaming and Redis sorted sets for leaderboard computation. When a scoring event arrives (e.g., a player scores a six), we: (1) calculate fantasy points, (2) atomically increment the player's score in Redis, (3) use a pre-computed reverse index (player-to-teams mapping) to find all affected teams, (4) batch-update team scores using Redis pipeline commands, (5) update leaderboard sorted sets. The entire flow is event-driven and non-blocking, achieving sub-500ms propagation.
Q3: How would you prevent a user from creating multiple accounts to gain unfair advantage?
Answer: Multi-layer detection: (1) Device fingerprinting (browser/device ID, screen resolution, installed fonts), (2) IP analysis and velocity checks, (3) Phone number and email verification with OTP, (4) KYC with PAN/Aadhaar deduplication, (5) Behavioral analysis — similar team creation patterns, (6) Graph analysis — detecting connected components of accounts that always join the same contests, (7) ML model trained on known fraud cases. High-risk accounts are flagged for manual review before withdrawals are processed.
Q4: Design the database schema for handling 5 billion team entries per year with efficient querying.
Answer: I would shard by match_id (16 shards) for teams and contests, and by user_id (8 shards) for wallets and profiles. Teams and contests share the same shard key so queries like "get all teams for a contest" are single-shard operations. Use partitioned tables with monthly partitions for time-series data. Hot data (recent matches) stays in SSD-backed primary storage, cold data (older than 90 days) moves to S3/Glacier via automated lifecycle policies. ClickHouse handles analytical queries on historical data.
Q5: How do you handle the financial transaction system to ensure no money is lost or double-credited?
Answer: The wallet service uses event sourcing with double-entry bookkeeping. Every state change (deposit, join, win, withdrawal) is an immutable event in an append-only log. The current balance is derived by replaying events. We use PostgreSQL transactions with serializable isolation for contest joins (preventing double-entry into full contests). Withdrawals go through a state machine (requested, verified, processing, completed) with idempotency keys. Daily reconciliation jobs verify that sum(debits) = sum(credits) across all wallets.
Q6: How would you design the leaderboard system for a grand league with 2 million entries?
Answer: Redis sorted sets are ideal here. ZADD updates a team's score atomically, and ZREVRANK gets a user's rank in O(log N). For 2 million entries, a sorted set uses ~100 MB. The leaderboard service maintains one sorted set per contest. For displaying the full leaderboard, we use ZREVRANGE with pagination (top 100, then lazy-load). For a user's personal rank, ZREVRANK gives O(log N) lookup. We also maintain a "change feed" — only rank changes are pushed via WebSocket, not full snapshots, keeping bandwidth manageable.
Q7: What happens if the sports data provider goes down during a live match?
Answer: We implement a multi-provider failover: (1) Primary provider (Sportradar) serves live data, (2) A health check monitors latency every 5 seconds, (3) If latency exceeds 500ms or connection drops, circuit breaker trips, (4) Secondary provider (Opta) is activated within 2 seconds, (5) Score reconciliation runs after match to merge data from both providers. Raw events from all providers are stored in Kafka for audit. The scoring engine is designed to handle out-of-order events and duplicate removal via event IDs.
Q8: How do you handle contest lock time and prevent edits after the match starts?
Answer: A distributed scheduler (using Redis keys with TTL + background workers) checks every 30 seconds for contests approaching lock time. At lock time: (1) Contest status transitions to LOCKED via atomic CAS operation, (2) A snapshot of all teams is taken and stored in a immutable snapshot table, (3) The scoring engine receives a ContestLocked event and loads all team compositions, (4) Any subsequent edit requests are rejected with HTTP 409 Conflict. The snapshot is critical — it decouples the scoring pipeline from the live database.
Q9: How would you scale the notification system to send 100 million push notifications in 5 minutes?
Answer: Fan-out through Kafka: the notification service publishes to a Kafka topic partitioned by user_id. 50 consumer instances each pull from their assigned partition, batch messages (1000 per batch), and send via FCM (Android) and APNs (iOS) using their respective batch APIs. FCM supports 500 messages per batch request. At 50 instances x 20 batch requests/second x 500 messages = 500K notifications/second. 100M notifications take ~200 seconds (3.3 minutes). Priority queues ensure match-critical notifications go first.
Q10: Explain how you would implement the wallet system to handle deposit, contest entry, and prize distribution atomically.
Answer: The wallet uses a ledger-based design with optimistic concurrency control. Each wallet has a version number. Operations use CAS: UPDATE wallet SET balance = balance - 100, version = version + 1 WHERE user_id = X AND version = V. If the update affects 0 rows (concurrent modification), retry with exponential backoff. For contest entry, we use a two-phase approach: (1) Debit wallet + create pending contest entry, (2) On success, confirm entry. On failure, rollback wallet. Prize distribution uses an idempotent batch job — each payout has a unique contest_id+rank key that prevents double-crediting.
Q11: How do you handle data consistency between the scoring engine and the leaderboard during high-velocity score updates?
Answer: We accept eventual consistency for leaderboards (within 1-2 seconds). The scoring engine writes player scores to Redis first, then publishes score update events to Kafka. The leaderboard consumer processes these events and updates sorted sets. If the leaderboard consumer falls behind, it catches up by processing events in batches. The key insight: during a cricket match, scoring events arrive at ~1 per 30 seconds per match, but there are thousands of concurrent matches. We shard the Kafka topic by match_id to parallelize processing. Users see their rank within 1-2 seconds of a scoring event.
Q12: How would you design the system to support adding a new sport (like baseball) with minimal code changes?
Answer: Sport-agnostic architecture: (1) Each sport is defined as a configuration JSON that specifies scoring rules, team composition constraints, player roles, and match format. (2) The scoring engine reads sport config at startup and uses a rule engine (strategy pattern) to apply sport-specific logic. (3) The team validation service uses constraint definitions from the config. (4) The data ingestion layer has pluggable adapters per sport. Adding baseball means: writing a new data adapter, creating a scoring rules JSON, defining team constraints, and deploying the config. No core code changes needed.

24. Full C# Implementation (300+ Lines)

Below is a comprehensive production-grade C# implementation covering domain models, scoring engine, team validation, leaderboard service, wallet management, and the main platform orchestrator.

using System;
using System.Collections.Generic;
using System.Collections.Concurrent;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace FantasySportsPlatform.Core
{
    // ============================================================
    // ENUMS & DOMAIN ENUMERATIONS
    // ============================================================

    public enum Sport { Cricket, Football, Basketball, Kabaddi }

    public enum MatchStatus
    {
        Scheduled, Live, Completed, Abandoned
    }

    public enum ContestStatus
    {
        Created, Open, Filling, Full, Locked,
        InProgress, Completed, Settled, Cancelled
    }

    public enum ContestType
    {
        HeadToHead, SmallLeague, GrandLeague,
        FreeContest, PrivateLeague
    }

    public enum PlayerRole
    {
        Batsman, Bowler, AllRounder, WicketKeeper,
        Goalkeeper, Defender, Midfielder, Forward,
        Raider, DefenderKabaddi
    }

    public enum TransactionType
    {
        Deposit, Withdrawal, ContestEntry, ContestRefund,
        PrizeWinning, Bonus, AdminAdjustment
    }

    // ============================================================
    // CORE DOMAIN MODELS
    // ============================================================

    public class Player
    {
        public int PlayerId { get; set; }
        public string Name { get; set; }
        public Sport Sport { get; set; }
        public string TeamName { get; set; }
        public PlayerRole Role { get; set; }
        public decimal Credits { get; set; }
        public decimal AveragePoints { get; set; }
        public decimal SelectionPercentage { get; set; }
        public bool IsActive { get; set; } = true;

        public override string ToString() =>
            $"[{PlayerId}] {Name} ({Role}) - " +
            $"{Credits} credits, {AveragePoints} avg pts";
    }

    public class Match
    {
        public int MatchId { get; set; }
        public Sport Sport { get; set; }
        public string TeamA { get; set; }
        public string TeamB { get; set; }
        public DateTime StartTime { get; set; }
        public MatchStatus Status { get; set; }
        public string Venue { get; set; }
        public List<Player> Players { get; set; } = new();
        public string DisplayName => $"{TeamA} vs {TeamB}";
    }

    public class Contest
    {
        public int ContestId { get; set; }
        public int MatchId { get; set; }
        public ContestType Type { get; set; }
        public ContestStatus Status { get; set; }
        public decimal EntryFee { get; set; }
        public int TotalSpots { get; set; }
        public int FilledSpots { get; set; }
        public decimal PrizePool { get; set; }
        public decimal PlatformCommission => PrizePool * 0.15m;
        public decimal WinnerPool => PrizePool - PlatformCommission;
        public int MaxEntriesPerUser { get; set; } = 1;
        public Dictionary<int, decimal> PrizeDistribution
            { get; set; } = new();
        public bool HasSpots => FilledSpots < TotalSpots;
        public bool IsFull => FilledSpots >= TotalSpots;
    }

    public class FantasyTeam
    {
        public int TeamId { get; set; }
        public int UserId { get; set; }
        public int MatchId { get; set; }
        public int ContestId { get; set; }
        public string TeamName { get; set; }
        public int CaptainPlayerId { get; set; }
        public int ViceCaptainPlayerId { get; set; }
        public List<int> PlayerIds { get; set; } = new();
        public decimal TotalPoints { get; set; }
        public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
        public DateTime? LastUpdatedAt { get; set; }
        public decimal CaptainMultiplier => 2.0m;
        public decimal ViceCaptainMultiplier => 1.5m;
    }

    public class ScoringEvent
    {
        public int MatchId { get; set; }
        public int PlayerId { get; set; }
        public string EventType { get; set; }
        public decimal PointsDelta { get; set; }
        public string Description { get; set; }
        public DateTime Timestamp { get; set; } = DateTime.UtcNow;
        public int OverNumber { get; set; }
        public int BallNumber { get; set; }
    }

    public class LeaderboardEntry
    {
        public int Rank { get; set; }
        public int TeamId { get; set; }
        public string TeamName { get; set; }
        public string UserName { get; set; }
        public decimal Score { get; set; }
        public bool IsCurrentUser { get; set; }
        public decimal Winnings { get; set; }
    }

    public class Transaction
    {
        public long TransactionId { get; set; }
        public int UserId { get; set; }
        public TransactionType Type { get; set; }
        public decimal Amount { get; set; }
        public decimal BalanceBefore { get; set; }
        public decimal BalanceAfter { get; set; }
        public string ReferenceId { get; set; }
        public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
        public bool IsCompleted { get; set; }
    }

    public class Wallet
    {
        public int UserId { get; set; }
        public decimal UnlockedBalance { get; set; }
        public decimal BonusBalance { get; set; }
        public decimal WinningsBalance { get; set; }
        public decimal ContestsPending { get; set; }
        public long Version { get; set; }
        public decimal TotalBalance =>
            UnlockedBalance + BonusBalance + WinningsBalance;
        public decimal WithdrawableBalance =>
            UnlockedBalance + WinningsBalance;
    }
    // ============================================================
    // SCORING RULE ENGINE
    // ============================================================

    public class ScoringRule
    {
        public string EventType { get; set; }
        public decimal Points { get; set; }
        public Func<ScoringEvent, decimal> CustomCalculation
            { get; set; }
    }

    public class ScoringRuleEngine
    {
        private readonly Dictionary<Sport,
            List<ScoringRule>> _rules = new();

        public ScoringRuleEngine()
        {
            InitializeCricketRules();
            InitializeFootballRules();
            InitializeBasketballRules();
            InitializeKabaddiRules();
        }

        private void InitializeCricketRules()
        {
            _rules[Sport.Cricket] = new List<ScoringRule>
            {
                new() { EventType = "run", Points = 1 },
                new() { EventType = "four", Points = 4 },
                new() { EventType = "six", Points = 6 },
                new() { EventType = "wicket", Points = 25 },
                new() { EventType = "catch", Points = 8 },
                new() { EventType = "stumping", Points = 12 },
                new() { EventType = "run_out", Points = 12 },
                new() { EventType = "maiden_over", Points = 12 },
                new() { EventType = "dot_ball", Points = 1 },
                new() { EventType = "thirty_run_bonus", Points = 4 },
                new() { EventType = "fifty_run_bonus", Points = 8 },
                new() { EventType = "hundred_run_bonus", Points = 16 },
                new() { EventType = "five_wicket_bonus", Points = 16 },
                new() { EventType = "ten_wicket_bonus", Points = 32 },
                new() { EventType = "wide", Points = -1 },
                new() { EventType = "no_ball", Points = -1 },
                new() { EventType = "duck", Points = -2 },
                new()
                {
                    EventType = "strike_rate_bonus",
                    Points = 0,
                    CustomCalculation = evt => 6
                },
                new()
                {
                    EventType = "economy_rate_bonus",
                    Points = 0,
                    CustomCalculation = evt => 6
                }
            };
        }

        private void InitializeFootballRules()
        {
            _rules[Sport.Football] = new List<ScoringRule>
            {
                new() { EventType = "goal", Points = 10 },
                new() { EventType = "assist", Points = 5 },
                new() { EventType = "clean_sheet", Points = 4 },
                new() { EventType = "save", Points = 2 },
                new() { EventType = "yellow_card", Points = -2 },
                new() { EventType = "red_card", Points = -5 },
                new() { EventType = "own_goal", Points = -3 },
                new() { EventType = "penalty_saved", Points = 5 },
                new() { EventType = "penalty_missed", Points = -3 },
                new() { EventType = "minutes_played_90", Points = 2 },
                new() { EventType = "man_of_match", Points = 10 },
            };
        }

        private void InitializeBasketballRules()
        {
            _rules[Sport.Basketball] = new List<ScoringRule>
            {
                new() { EventType = "point_scored", Points = 1 },
                new() { EventType = "rebound", Points = 1.5m },
                new() { EventType = "assist", Points = 2 },
                new() { EventType = "steal", Points = 3 },
                new() { EventType = "block", Points = 3 },
                new() { EventType = "turnover", Points = -1 },
                new() { EventType = "double_double", Points = 5 },
                new() { EventType = "triple_double", Points = 15 },
            };
        }

        private void InitializeKabaddiRules()
        {
            _rules[Sport.Kabaddi] = new List<ScoringRule>
            {
                new() { EventType = "raid_point", Points = 2 },
                new() { EventType = "tackle_point", Points = 3 },
                new() { EventType = "all_out_bonus", Points = 5 },
                new() { EventType = "super_raid", Points = 4 },
                new() { EventType = "super_tackle", Points = 3 },
                new() { EventType = "do_or_die_raid", Points = 1 },
                new() { EventType = "empty_raid", Points = -1 },
                new() { EventType = "all_out_conceded", Points = -3 },
            };
        }

        public decimal CalculatePoints(
            Sport sport, ScoringEvent evt)
        {
            if (!_rules.ContainsKey(sport))
                throw new ArgumentException(
                    $"No scoring rules for {sport}");

            var rule = _rules[sport].FirstOrDefault(
                r => r.EventType == evt.EventType);
            if (rule == null) return 0;

            return rule.CustomCalculation != null
                ? rule.CustomCalculation(evt)
                : rule.Points;
        }
    }

    // ============================================================
    // TEAM VALIDATOR
    // ============================================================

    public class TeamConstraints
    {
        public int TeamSize { get; set; }
        public int MaxFromSameTeam { get; set; }
        public decimal MaxCredits { get; set; }
        public Dictionary<PlayerRole, (int Min, int Max)>
            RoleConstraints { get; set; } = new();
    }

    public class TeamValidationResult
    {
        public bool IsValid { get; set; }
        public List<string> Errors { get; set; } = new();

        public static TeamValidationResult Success() =>
            new() { IsValid = true };

        public static TeamValidationResult Failure(
            params string[] errors) =>
            new() { IsValid = false, Errors = errors.ToList() };
    }

    public class TeamValidator
    {
        private readonly Dictionary<Sport,
            TeamConstraints> _constraints = new();

        public TeamValidator()
        {
            _constraints[Sport.Cricket] = new TeamConstraints
            {
                TeamSize = 11,
                MaxFromSameTeam = 7,
                MaxCredits = 100,
                RoleConstraints = new()
                {
                    { PlayerRole.WicketKeeper, (1, 3) },
                    { PlayerRole.Batsman, (3, 6) },
                    { PlayerRole.AllRounder, (1, 4) },
                    { PlayerRole.Bowler, (3, 6) }
                }
            };

            _constraints[Sport.Football] = new TeamConstraints
            {
                TeamSize = 11,
                MaxFromSameTeam = 7,
                MaxCredits = 100,
                RoleConstraints = new()
                {
                    { PlayerRole.Goalkeeper, (1, 1) },
                    { PlayerRole.Defender, (3, 5) },
                    { PlayerRole.Midfielder, (3, 5) },
                    { PlayerRole.Forward, (1, 3) }
                }
            };

            _constraints[Sport.Basketball] = new TeamConstraints
            {
                TeamSize = 8,
                MaxFromSameTeam = 5,
                MaxCredits = 100,
                RoleConstraints = new()
                {
                    { PlayerRole.Batsman, (1, 8) }
                }
            };

            _constraints[Sport.Kabaddi] = new TeamConstraints
            {
                TeamSize = 7,
                MaxFromSameTeam = 4,
                MaxCredits = 100,
                RoleConstraints = new()
                {
                    { PlayerRole.Raider, (2, 4) },
                    { PlayerRole.DefenderKabaddi, (3, 5) },
                    { PlayerRole.AllRounder, (0, 2) }
                }
            };
        }

        public TeamValidationResult Validate(
            FantasyTeam team,
            List<Player> availablePlayers,
            Sport sport)
        {
            if (!_constraints.ContainsKey(sport))
                return TeamValidationResult.Failure(
                    $"Unsupported sport: {sport}");

            var c = _constraints[sport];

            if (team.PlayerIds.Count != c.TeamSize)
                return TeamValidationResult.Failure(
                    $"Team must have exactly {c.TeamSize} " +
                    $"players. Currently has " +
                    $"{team.PlayerIds.Count}.");

            var selected = availablePlayers
                .Where(p => team.PlayerIds.Contains(p.PlayerId))
                .ToList();

            if (selected.Count != team.PlayerIds.Count)
            {
                var missing = team.PlayerIds
                    .Except(selected.Select(p => p.PlayerId));
                return TeamValidationResult.Failure(
                    $"Players not found: " +
                    $"{string.Join(", ", missing)}");
            }

            var totalCredits = selected.Sum(p => p.Credits);
            if (totalCredits > c.MaxCredits)
                return TeamValidationResult.Failure(
                    $"Total credits ({totalCredits}) exceed " +
                    $"limit ({c.MaxCredits})");

            if (!team.PlayerIds.Contains(team.CaptainPlayerId))
                return TeamValidationResult.Failure(
                    "Captain must be one of selected players");

            if (!team.PlayerIds.Contains(
                team.ViceCaptainPlayerId))
                return TeamValidationResult.Failure(
                    "VC must be one of selected players");

            if (team.CaptainPlayerId ==
                team.ViceCaptainPlayerId)
                return TeamValidationResult.Failure(
                    "Captain and VC must be different players");

            var maxFromSame = selected
                .GroupBy(p => p.TeamName)
                .Max(g => g.Count());
            if (maxFromSame > c.MaxFromSameTeam)
                return TeamValidationResult.Failure(
                    $"Max {c.MaxFromSameTeam} from same team. " +
                    $"Got {maxFromSame}.");

            foreach (var (role, (min, max)) in
                c.RoleConstraints)
            {
                var count = selected.Count(
                    p => p.Role == role);
                if (count < min)
                    return TeamValidationResult.Failure(
                        $"Need at least {min} {role}(s). " +
                        $"Got {count}.");
                if (count > max)
                    return TeamValidationResult.Failure(
                        $"Max {max} {role}(s) allowed. " +
                        $"Got {count}.");
            }

            return TeamValidationResult.Success();
        }
    }
    // ============================================================
    // LEADERBOARD SERVICE (Redis Sorted Set Simulation)
    // ============================================================

    public class LeaderboardService
    {
        private readonly ConcurrentDictionary<string,
            SortedDictionary<int, decimal>>
                _leaderboards = new();

        private string GetKey(int contestId) =>
            $"leaderboard:{contestId}";

        public void UpdateScore(
            int contestId, int teamId, decimal newScore)
        {
            var key = GetKey(contestId);
            _leaderboards.AddOrUpdate(key,
                _ =>
                {
                    var dict =
                        new SortedDictionary<int, decimal>
                    { [teamId] = newScore };
                    return dict;
                },
                (_, existing) =>
                {
                    existing[teamId] = newScore;
                    return existing;
                });
        }

        public int GetRank(int contestId, int teamId)
        {
            var key = GetKey(contestId);
            if (!_leaderboards.ContainsKey(key))
                return -1;

            var sorted = _leaderboards[key]
                .OrderByDescending(kvp => kvp.Value)
                .ToList();
            var idx = sorted.FindIndex(
                kvp => kvp.Key == teamId);
            return idx >= 0 ? idx + 1 : -1;
        }

        public List<LeaderboardEntry> GetTopN(
            int contestId, int count = 100)
        {
            var key = GetKey(contestId);
            if (!_leaderboards.ContainsKey(key))
                return new List<LeaderboardEntry>();

            return _leaderboards[key]
                .OrderByDescending(kvp => kvp.Value)
                .Take(count)
                .Select((kvp, i) => new LeaderboardEntry
                {
                    Rank = i + 1,
                    TeamId = kvp.Key,
                    Score = kvp.Value
                })
                .ToList();
        }

        public List<LeaderboardEntry> GetNearby(
            int contestId, int teamId, int range = 20)
        {
            var rank = GetRank(contestId, teamId);
            if (rank < 0)
                return new List<LeaderboardEntry>();

            var key = GetKey(contestId);
            return _leaderboards[key]
                .OrderByDescending(kvp => kvp.Value)
                .Skip(Math.Max(0, rank - 1 - range))
                .Take(range * 2 + 1)
                .Select((kvp, i) => new LeaderboardEntry
                {
                    Rank = Math.Max(1, rank - range) + i,
                    TeamId = kvp.Key,
                    Score = kvp.Value,
                    IsCurrentUser = kvp.Key == teamId
                })
                .ToList();
        }

        public int GetContestSize(int contestId)
        {
            var key = GetKey(contestId);
            return _leaderboards.ContainsKey(key)
                ? _leaderboards[key].Count
                : 0;
        }
    }

    // ============================================================
    // WALLET SERVICE (Event Sourced, Thread-Safe)
    // ============================================================

    public class WalletService
    {
        private readonly ConcurrentDictionary<int,
            Wallet> _wallets = new();
        private readonly ConcurrentDictionary<int,
            List<Transaction>> _transactionLog = new();
        private long _nextTxId = 1;

        public Wallet GetOrCreateWallet(int userId)
        {
            return _wallets.GetOrAdd(userId,
                _ => new Wallet
                {
                    UserId = userId,
                    UnlockedBalance = 0,
                    BonusBalance = 0,
                    WinningsBalance = 0,
                    ContestsPending = 0,
                    Version = 0
                });
        }

        public bool Deposit(
            int userId, decimal amount, string refId)
        {
            if (amount <= 0) return false;
            var wallet = GetOrCreateWallet(userId);
            lock (wallet)
            {
                var tx = new Transaction
                {
                    TransactionId = Interlocked.Increment(
                        ref _nextTxId),
                    UserId = userId,
                    Type = TransactionType.Deposit,
                    Amount = amount,
                    BalanceBefore = wallet.TotalBalance,
                    ReferenceId = refId,
                    IsCompleted = true
                };
                wallet.UnlockedBalance += amount;
                wallet.Version++;
                tx.BalanceAfter = wallet.TotalBalance;
                _transactionLog.AddOrUpdate(userId,
                    _ => new List<Transaction> { tx },
                    (_, list) =>
                    { list.Add(tx); return list; });
                return true;
            }
        }

        public bool JoinContest(
            int userId, int contestId, decimal fee)
        {
            var wallet = GetOrCreateWallet(userId);
            lock (wallet)
            {
                if (wallet.UnlockedBalance < fee)
                    return false;
                var tx = new Transaction
                {
                    TransactionId = Interlocked.Increment(
                        ref _nextTxId),
                    UserId = userId,
                    Type = TransactionType.ContestEntry,
                    Amount = -fee,
                    BalanceBefore = wallet.TotalBalance,
                    ReferenceId = $"contest_{contestId}",
                    IsCompleted = true
                };
                wallet.UnlockedBalance -= fee;
                wallet.ContestsPending += fee;
                wallet.Version++;
                tx.BalanceAfter = wallet.TotalBalance;
                _transactionLog.AddOrUpdate(userId,
                    _ => new List<Transaction> { tx },
                    (_, list) =>
                    { list.Add(tx); return list; });
                return true;
            }
        }

        public bool CreditWinnings(
            int userId, int contestId, decimal amount)
        {
            var wallet = GetOrCreateWallet(userId);
            lock (wallet)
            {
                var pending = Math.Min(
                    wallet.ContestsPending, amount);
                wallet.ContestsPending -= pending;
                var tx = new Transaction
                {
                    TransactionId = Interlocked.Increment(
                        ref _nextTxId),
                    UserId = userId,
                    Type = TransactionType.PrizeWinning,
                    Amount = amount,
                    BalanceBefore = wallet.TotalBalance,
                    ReferenceId = $"prize_{contestId}",
                    IsCompleted = true
                };
                wallet.WinningsBalance += amount;
                wallet.Version++;
                tx.BalanceAfter = wallet.TotalBalance;
                _transactionLog.AddOrUpdate(userId,
                    _ => new List<Transaction> { tx },
                    (_, list) =>
                    { list.Add(tx); return list; });
                return true;
            }
        }

        public bool RefundContestEntry(
            int userId, int contestId, decimal amount)
        {
            var wallet = GetOrCreateWallet(userId);
            lock (wallet)
            {
                wallet.ContestsPending -= amount;
                var tx = new Transaction
                {
                    TransactionId = Interlocked.Increment(
                        ref _nextTxId),
                    UserId = userId,
                    Type = TransactionType.ContestRefund,
                    Amount = amount,
                    BalanceBefore = wallet.TotalBalance,
                    ReferenceId = $"refund_{contestId}",
                    IsCompleted = true
                };
                wallet.UnlockedBalance += amount;
                wallet.Version++;
                tx.BalanceAfter = wallet.TotalBalance;
                _transactionLog.AddOrUpdate(userId,
                    _ => new List<Transaction> { tx },
                    (_, list) =>
                    { list.Add(tx); return list; });
                return true;
            }
        }

        public List<Transaction> GetHistory(
            int userId, int limit = 50)
        {
            if (!_transactionLog.ContainsKey(userId))
                return new List<Transaction>();
            return _transactionLog[userId]
                .OrderByDescending(tx => tx.CreatedAt)
                .Take(limit)
                .ToList();
        }

        public bool ValidateBalance(int userId)
        {
            if (!_transactionLog.ContainsKey(userId))
                return true;
            var wallet = GetOrCreateWallet(userId);
            var computed = _transactionLog[userId]
                .Sum(tx => tx.Amount);
            return Math.Abs(
                wallet.TotalBalance - computed) < 0.01m;
        }
    }
    // ============================================================
    // FANTASY SPORTS PLATFORM - MAIN ORCHESTRATOR
    // ============================================================

    public class FantasySportsPlatform
    {
        private readonly ScoringRuleEngine _scoringEngine;
        private readonly TeamValidator _teamValidator;
        private readonly LeaderboardService _leaderboard;
        private readonly WalletService _walletService;
        private readonly ConcurrentDictionary<int,
            Match> _matches;
        private readonly ConcurrentDictionary<int,
            Contest> _contests;
        private readonly ConcurrentDictionary<int,
            FantasyTeam> _teams;
        private readonly ConcurrentDictionary<int,
            List<int>> _playerTeamIndex;
        private int _nextTeamId = 1;

        public FantasySportsPlatform()
        {
            _scoringEngine = new ScoringRuleEngine();
            _teamValidator = new TeamValidator();
            _leaderboard = new LeaderboardService();
            _walletService = new WalletService();
            _matches = new();
            _contests = new();
            _teams = new();
            _playerTeamIndex = new();
        }

        public Match CreateMatch(Match match)
        {
            _matches[match.MatchId] = match;
            Console.WriteLine(
                $"  Match Created: {match.DisplayName} " +
                $"({match.Sport}) at " +
                $"{match.StartTime:u}");
            return match;
        }

        public void StartMatch(int matchId)
        {
            if (!_matches.ContainsKey(matchId)) return;
            _matches[matchId].Status = MatchStatus.Live;
            Console.WriteLine(
                $"  Match Started: " +
                $"{_matches[matchId].DisplayName}");

            foreach (var c in _contests.Values
                .Where(c => c.MatchId == matchId &&
                    c.Status != ContestStatus.Cancelled))
                c.Status = ContestStatus.InProgress;
        }

        public void CompleteMatch(int matchId)
        {
            if (!_matches.ContainsKey(matchId)) return;
            _matches[matchId].Status =
                MatchStatus.Completed;
            Console.WriteLine(
                $"  Match Completed: " +
                $"{_matches[matchId].DisplayName}");
        }

        public Contest CreateContest(Contest contest)
        {
            _contests[contest.ContestId] = contest;
            Console.WriteLine(
                $"  Contest Created: " +
                $"#{contest.ContestId} " +
                $"({contest.Type}) - " +
                $"Rs{contest.EntryFee} entry, " +
                $"{contest.TotalSpots} spots, " +
                $"Rs{contest.PrizePool} pool");
            return contest;
        }

        public void GeneratePrizeDistribution(
            Contest contest)
        {
            var dist = new Dictionary<int, decimal>();

            if (contest.Type ==
                ContestType.HeadToHead)
            {
                dist[1] = contest.WinnerPool;
            }
            else if (contest.TotalSpots <= 10)
            {
                int winners = Math.Max(1,
                    contest.TotalSpots / 3);
                decimal each = contest.WinnerPool
                    / winners;
                for (int i = 1; i <= winners; i++)
                    dist[i] = Math.Round(each, 2);
            }
            else
            {
                int top10 = Math.Min(10,
                    contest.TotalSpots);
                decimal top10Pool =
                    contest.WinnerPool * 0.70m;
                decimal restPool =
                    contest.WinnerPool * 0.30m;
                decimal[] pcts = {
                    0.30m, 0.18m, 0.12m, 0.08m,
                    0.06m, 0.05m, 0.04m, 0.03m,
                    0.02m, 0.02m
                };
                for (int i = 0; i < top10; i++)
                    dist[i + 1] = Math.Round(
                        top10Pool * pcts[i], 2);

                if (contest.TotalSpots > 10)
                {
                    int rest = Math.Min(
                        contest.TotalSpots - 10,
                        (int)(contest.TotalSpots * 0.3));
                    if (rest > 0)
                    {
                        decimal each = Math.Round(
                            restPool / rest, 2);
                        for (int i = 10;
                            i < 10 + rest; i++)
                            dist[i + 1] = each;
                    }
                }
            }
            contest.PrizeDistribution = dist;
        }

        public (FantasyTeam team,
            TeamValidationResult result)
            CreateTeam(
                int userId, int matchId,
                int contestId, string teamName,
                List<int> playerIds,
                int captainId, int vcId)
        {
            if (!_matches.ContainsKey(matchId))
                return (null,
                    TeamValidationResult.Failure(
                        "Match not found"));
            if (!_contests.ContainsKey(contestId))
                return (null,
                    TeamValidationResult.Failure(
                        "Contest not found"));

            var contest = _contests[contestId];
            if (contest.Status == ContestStatus.Locked ||
                contest.Status ==
                    ContestStatus.InProgress ||
                contest.Status ==
                    ContestStatus.Completed)
                return (null,
                    TeamValidationResult.Failure(
                        "Contest is locked"));

            var team = new FantasyTeam
            {
                TeamId = Interlocked.Increment(
                    ref _nextTeamId),
                UserId = userId,
                MatchId = matchId,
                ContestId = contestId,
                TeamName = teamName,
                PlayerIds = playerIds,
                CaptainPlayerId = captainId,
                ViceCaptainPlayerId = vcId
            };

            var match = _matches[matchId];
            var validation = _teamValidator.Validate(
                team, match.Players, match.Sport);
            if (!validation.IsValid)
                return (team, validation);

            if (contest.EntryFee > 0)
            {
                bool ok = _walletService.JoinContest(
                    userId, contestId,
                    contest.EntryFee);
                if (!ok)
                    return (null,
                        TeamValidationResult.Failure(
                            "Insufficient balance"));
            }

            _teams[team.TeamId] = team;
            foreach (var pid in playerIds)
            {
                _playerTeamIndex.AddOrUpdate(pid,
                    _ => new List<int>
                        { team.TeamId },
                    (_, list) =>
                    {
                        lock (list)
                            list.Add(team.TeamId);
                        return list;
                    });
            }
            contest.FilledSpots++;
            if (contest.IsFull)
                contest.Status = ContestStatus.Full;

            _leaderboard.UpdateScore(
                contestId, team.TeamId, 0);
            Console.WriteLine(
                $"  Team Created: #{team.TeamId} " +
                $"\"{teamName}\" by User {userId} " +
                $"(C:{captainId} VC:{vcId}) " +
                $"[{playerIds.Count} players]");
            return (team, validation);
        }

        public void ProcessScoringEvent(
            ScoringEvent evt)
        {
            if (!_matches.ContainsKey(evt.MatchId))
                return;
            var match = _matches[evt.MatchId];
            decimal pts = _scoringEngine
                .CalculatePoints(match.Sport, evt);
            if (pts == 0) return;
            if (!_playerTeamIndex.ContainsKey(
                evt.PlayerId)) return;

            var affected = _playerTeamIndex[
                evt.PlayerId].ToList();
            foreach (var tid in affected)
            {
                if (!_teams.ContainsKey(tid)) continue;
                var team = _teams[tid];
                decimal p = pts;
                if (team.CaptainPlayerId == evt.PlayerId)
                    p *= team.CaptainMultiplier;
                else if (team.ViceCaptainPlayerId ==
                    evt.PlayerId)
                    p *= team.ViceCaptainMultiplier;
                team.TotalPoints += p;
                team.LastUpdatedAt = DateTime.UtcNow;
                _leaderboard.UpdateScore(
                    team.ContestId,
                    team.TeamId,
                    team.TotalPoints);
            }
        }

        public void SettleContest(int contestId)
        {
            if (!_contests.ContainsKey(contestId))
                return;
            var c = _contests[contestId];
            Console.WriteLine(
                $"\n  === SETTLING CONTEST " +
                $"#{contestId} ===");
            Console.WriteLine(
                $"    Prize Pool: Rs{c.PrizePool}," +
                $" Commission: " +
                $"Rs{c.PlatformCommission}");

            var lb = _leaderboard.GetTopN(
                contestId, c.TotalSpots);
            foreach (var e in lb)
            {
                if (!c.PrizeDistribution
                    .ContainsKey(e.Rank)) continue;
                decimal prize =
                    c.PrizeDistribution[e.Rank];
                e.Winnings = prize;
                var team = _teams.Values.FirstOrDefault(
                    t => t.TeamId == e.TeamId);
                if (team != null)
                    _walletService.CreditWinnings(
                        team.UserId, contestId, prize);
                Console.WriteLine(
                    $"    Rank #{e.Rank}: " +
                    $"Team {e.TeamId} " +
                    $"({e.Score:F1} pts) " +
                    $"- Rs{prize}");
            }
            c.Status = ContestStatus.Settled;
            Console.WriteLine(
                $"  Contest #{contestId} settled");
        }

        public List<LeaderboardEntry>
            GetLeaderboard(int cid, int n = 10)
            => _leaderboard.GetTopN(cid, n);

        public Wallet GetWallet(int uid)
            => _walletService.GetOrCreateWallet(uid);

        public Contest GetContest(int cid)
            => _contests.ContainsKey(cid)
                ? _contests[cid] : null;
    }
    // ============================================================
    // DEMO / MAIN PROGRAM
    // ============================================================

    public static class Program
    {
        public static void Main(string[] args)
        {
            Console.WriteLine(
                "================================================");
            Console.WriteLine(
                "  FANTASY SPORTS PLATFORM");
            Console.WriteLine(
                "  System Design - Full C# Implementation");
            Console.WriteLine(
                "  Ayodhyya | 300+ Lines");
            Console.WriteLine(
                "================================================\n");

            var platform = new FantasySportsPlatform();

            // -- Step 1: Create Match --
            Console.WriteLine(
                "[STEP 1] Creating IPL Match...");
            var match = new Match
            {
                MatchId = 1001,
                Sport = Sport.Cricket,
                TeamA = "Mumbai Indians",
                TeamB = "Chennai Super Kings",
                StartTime = DateTime.UtcNow
                    .AddHours(2),
                Status = MatchStatus.Scheduled,
                Venue = "Wankhede Stadium",
                Players = CreateSamplePlayers()
            };
            platform.CreateMatch(match);

            // -- Step 2: Create Contests --
            Console.WriteLine(
                "\n[STEP 2] Creating Contests...");
            var grandLeague = platform.CreateContest(
                new Contest
            {
                ContestId = 5001,
                MatchId = 1001,
                Type = ContestType.GrandLeague,
                Status = ContestStatus.Open,
                EntryFee = 49,
                TotalSpots = 100,
                FilledSpots = 0,
                PrizePool = 4900
            });
            platform.GeneratePrizeDistribution(
                grandLeague);

            var h2h = platform.CreateContest(new Contest
            {
                ContestId = 5002,
                MatchId = 1001,
                Type = ContestType.HeadToHead,
                Status = ContestStatus.Open,
                EntryFee = 100,
                TotalSpots = 2,
                FilledSpots = 0,
                PrizePool = 200
            });
            platform.GeneratePrizeDistribution(h2h);

            // -- Step 3: Fund Wallets --
            Console.WriteLine(
                "\n[STEP 3] Funding Wallets...");
            for (int uid = 101; uid <= 105; uid++)
            {
                platform.GetOrCreateWallet(uid);
                platform.Deposit(uid, 5000,
                    $"UPI_{uid}");
                Console.WriteLine(
                    $"    User {uid} wallet: " +
                    $"{platform.GetWallet(uid)}");
            }

            // -- Step 4: Create Teams --
            Console.WriteLine(
                "\n[STEP 4] Creating Teams...");
            var p = match.Players;
            var teams = new[]
            {
                (101, "MiFan11",
                    new[]{p[0].PlayerId, p[2].PlayerId,
                        p[4].PlayerId, p[6].PlayerId,
                        p[8].PlayerId, p[10].PlayerId,
                        p[12].PlayerId, p[14].PlayerId,
                        p[16].PlayerId, p[18].PlayerId,
                        p[20].PlayerId},
                    p[4].PlayerId, p[8].PlayerId),
                (102, "CskArmy",
                    new[]{p[1].PlayerId, p[3].PlayerId,
                        p[5].PlayerId, p[7].PlayerId,
                        p[9].PlayerId, p[11].PlayerId,
                        p[13].PlayerId, p[15].PlayerId,
                        p[17].PlayerId, p[19].PlayerId,
                        p[21].PlayerId},
                    p[5].PlayerId, p[9].PlayerId),
                (103, "CricketKing",
                    new[]{p[0].PlayerId, p[1].PlayerId,
                        p[4].PlayerId, p[5].PlayerId,
                        p[8].PlayerId, p[9].PlayerId,
                        p[12].PlayerId, p[13].PlayerId,
                        p[16].PlayerId, p[17].PlayerId,
                        p[20].PlayerId},
                    p[4].PlayerId, p[12].PlayerId),
                (104, "PowerPlay",
                    new[]{p[2].PlayerId, p[3].PlayerId,
                        p[6].PlayerId, p[7].PlayerId,
                        p[10].PlayerId, p[11].PlayerId,
                        p[14].PlayerId, p[15].PlayerId,
                        p[18].PlayerId, p[19].PlayerId,
                        p[21].PlayerId},
                    p[10].PlayerId, p[14].PlayerId),
                (105, "AllStar",
                    new[]{p[0].PlayerId, p[3].PlayerId,
                        p[5].PlayerId, p[6].PlayerId,
                        p[9].PlayerId, p[10].PlayerId,
                        p[13].PlayerId, p[14].PlayerId,
                        p[17].PlayerId, p[18].PlayerId,
                        p[20].PlayerId},
                    p[5].PlayerId, p[9].PlayerId),
            };

            foreach (var t in teams)
            {
                platform.CreateTeam(
                    t.Item1, 1001, 5001, t.Item2,
                    t.Item3.ToList(), t.Item4, t.Item5);
            }

            // -- Step 5: Start Match --
            Console.WriteLine(
                "\n[STEP 5] Starting Match...");
            platform.StartMatch(1001);

            // -- Step 6: Process Scoring Events --
            Console.WriteLine(
                "\n[STEP 6] Processing Live Scores...");
            var events = new ScoringEvent[]
            {
                new() { MatchId=1001,
                    PlayerId=p[4].PlayerId,
                    EventType="four",
                    Description="CSK batsman hits 4" },
                new() { MatchId=1001,
                    PlayerId=p[5].PlayerId,
                    EventType="six",
                    Description="CSK captain six!" },
                new() { MatchId=1001,
                    PlayerId=p[4].PlayerId,
                    EventType="catch",
                    Description="MI fielder catches" },
                new() { MatchId=1001,
                    PlayerId=p[12].PlayerId,
                    EventType="wicket",
                    Description="MI bowler strikes!" },
                new() { MatchId=1001,
                    PlayerId=p[10].PlayerId,
                    EventType="run",
                    Description="MI batsman runs 2" },
                new() { MatchId=1001,
                    PlayerId=p[5].PlayerId,
                    EventType="fifty_run_bonus",
                    Description="CSK captain 50!" },
                new() { MatchId=1001,
                    PlayerId=p[12].PlayerId,
                    EventType="maiden_over",
                    Description="MI bowler maiden!" },
                new() { MatchId=1001,
                    PlayerId=p[9].PlayerId,
                    EventType="wicket",
                    Description="CSK bowler strikes!" },
                new() { MatchId=1001,
                    PlayerId=p[4].PlayerId,
                    EventType="duck",
                    Description="MI batsman ducks" },
                new() { MatchId=1001,
                    PlayerId=p[14].PlayerId,
                    EventType="six",
                    Description="MI all-rounder six!" },
            };

            foreach (var evt in events)
            {
                platform.ProcessScoringEvent(evt);
                Console.WriteLine(
                    $"    Event: {evt.Description} " +
                    $"(Player {evt.PlayerId})");
            }

            // -- Step 7: Show Leaderboard --
            Console.WriteLine(
                "\n[STEP 7] Grand League Leaderboard:");
            var lb = platform.GetLeaderboard(5001, 10);
            foreach (var e in lb)
            {
                Console.WriteLine(
                    $"    #{e.Rank} Team {e.TeamId} " +
                    $"- {e.Score:F1} pts");
            }

            // -- Step 8: Complete & Settle --
            Console.WriteLine(
                "\n[STEP 8] Completing Match...");
            platform.CompleteMatch(1001);
            platform.SettleContest(5001);

            // -- Step 9: Show Wallets --
            Console.WriteLine(
                "\n[STEP 9] Final Wallet Balances:");
            for (int uid = 101; uid <= 105; uid++)
            {
                var w = platform.GetWallet(uid);
                Console.WriteLine(
                    $"    User {uid}: {w} " +
                    $"(Valid: " +
                    $"{platform._walletService " +
                    $".ValidateBalance(uid)})");
            }

            Console.WriteLine(
                "\n================================================");
            Console.WriteLine(
                "  Platform demonstration complete!");
            Console.WriteLine(
                "================================================");
        }

        static List<Player> CreateSamplePlayers()
        {
            var players = new List<Player>();
            int id = 1;
            string[] mi = {
                "Rohit", "Ishan K", "Suryakumar",
                "Tilak V", "Hardik P", "Tim D",
                "Nehal W", "Piyush C", "Jasprit B",
                "Trent B", "Gerald C", "Akash M"
            };
            string[] csk = {
                "Ruturaj", "Devon C", "Ravindra J",
                "Shivam D", "MS D", "Rachin R",
                "Moeen A", "Deepak C", "Tushar D",
                "Maheesh T", "Matheesha P", "Shardul T"
            };

            var roles = new[] {
                PlayerRole.Batsman, PlayerRole.Batsman,
                PlayerRole.Batsman, PlayerRole.Batsman,
                PlayerRole.AllRounder,
                PlayerRole.AllRounder,
                PlayerRole.Batsman,
                PlayerRole.Bowler,
                PlayerRole.Bowler,
                PlayerRole.Bowler,
                PlayerRole.Bowler,
                PlayerRole.Bowler
            };

            var wk = new HashSet<string> {
                "Ishan K", "MS D"
            };
            var ar = new HashSet<string> {
                "Hardik P", "Ravindra J",
                "Shivam D", "Moeen A"
            };

            foreach (var name in mi)
            {
                PlayerRole role;
                if (wk.Contains(name))
                    role = PlayerRole.WicketKeeper;
                else if (ar.Contains(name))
                    role = PlayerRole.AllRounder;
                else
                    role = roles[id - 1];
                players.Add(new Player
                {
                    PlayerId = id++,
                    Name = name,
                    Sport = Sport.Cricket,
                    TeamName = "Mumbai Indians",
                    Role = role,
                    Credits = Math.Round(
                        (decimal)(12 - id * 0.3
                            + new Random(id).NextDouble()
                            * 3), 0),
                    AveragePoints = Math.Round(
                        (decimal)(5 + new Random(id)
                            .NextDouble() * 25), 1),
                    SelectionPercentage = Math.Round(
                        (decimal)(10 + new Random(id)
                            .NextDouble() * 60), 1)
                });
            }
            foreach (var name in csk)
            {
                PlayerRole role;
                if (wk.Contains(name))
                    role = PlayerRole.WicketKeeper;
                else if (ar.Contains(name))
                    role = PlayerRole.AllRounder;
                else
                    role = roles[id - 13];
                players.Add(new Player
                {
                    PlayerId = id++,
                    Name = name,
                    Sport = Sport.Cricket,
                    TeamName = "Chennai Super Kings",
                    Role = role,
                    Credits = Math.Round(
                        (decimal)(12 - (id-12) * 0.3
                            + new Random(id).NextDouble()
                            * 3), 0),
                    AveragePoints = Math.Round(
                        (decimal)(5 + new Random(id)
                            .NextDouble() * 25), 1),
                    SelectionPercentage = Math.Round(
                        (decimal)(10 + new Random(id)
                            .NextDouble() * 60), 1)
                });
            }
            return players;
        }
    }
}

25. Conclusion

Designing a fantasy sports platform at the scale of Dream11 (200M+ users) is one of the most challenging and rewarding systems design exercises. It combines real-time event processing, heavy computation, financial systems, social features, regulatory compliance, and extreme traffic scalability — all in a single platform.

Let us recap the key design decisions that make this system work at scale:

  • Event-driven architecture: Apache Kafka as the backbone for score ingestion, leaderboard updates, and notification fan-out ensures loose coupling and horizontal scalability.
  • Redis sorted sets for leaderboards: O(log N) score updates and rank queries, supporting millions of entries per contest with sub-millisecond latency.
  • Database sharding: Match-based sharding for contests and teams keeps related data co-located; user-based sharding for wallets ensures ACID financial transactions.
  • Scoring engine pipeline: A configurable, sport-agnostic scoring engine that processes live match events and propagates scores to millions of teams within 500ms.
  • Wallet service with event sourcing: Double-entry bookkeeping, optimistic concurrency control, and idempotent operations ensure financial correctness.
  • Multi-layer caching: CDN, Redis, and application-level caching reduce database load by 95% during peak traffic.
  • Anti-fraud systems: Device fingerprinting, graph analysis, and behavioral ML models protect platform integrity.
  • Compliance-first design: Geo-fencing, KYC, GST/TDS automation, and self-exclusion mechanisms ensure regulatory adherence.
Key Takeaway: The most critical insight in designing a fantasy sports platform is understanding the asymmetry of read vs write patterns. Writes (team creation, contest joins) are bursty but relatively low-volume compared to reads (leaderboard views, score checks). The entire architecture is optimized to serve reads from Redis while processing writes through Kafka-based pipelines. This CQRS-like approach is what makes 50K+ QPS reads possible while maintaining strong consistency for financial operations.

The C# implementation provided in this article demonstrates a working foundation with over 300 lines of production-quality code covering domain models, scoring rules for 4 sports, team validation with complex constraints, a Redis-backed leaderboard service, an event-sourced wallet system, and a full platform orchestrator with a demo program. This implementation can be extended with actual Redis connections, PostgreSQL repositories, and Kafka producers to become a production system.

Fantasy sports is not just a gaming platform — it is a sophisticated real-time distributed system that pushes the boundaries of modern software engineering. Mastering its design will prepare you for some of the most challenging system design interviews and real-world engineering problems.

Whether you are preparing for a senior engineering interview at a FAANG company or building the next Dream11 — understanding the architecture of a fantasy sports platform gives you deep insight into real-time systems, event-driven design, financial engineering, and building for extreme scale.
Further Reading:
  • Dream11 Engineering Blog: engineering.dream11.com
  • System Design Interview Volume 2 by Alex Xu
  • Designing Data-Intensive Applications by Martin Kleppmann
  • Redis Documentation: Sorted Sets
  • Apache Kafka: The Definitive Guide
  • Amazon AWS Well-Architected Framework for Gaming

Did you find this guide helpful?

Share it with other engineers preparing for system design interviews.

Tags: Fantasy Sports System Design, Dream11 Architecture, Real-Time Scoring Engine, Leaderboard System Design, Contest Management, C# Implementation, Distributed Systems, Interview Preparation