Extreme scalability: A regular Tuesday has 5M connected viewers across thousands of games. A Super Bowl draws 100M+ concurrent viewers on a single game. The system must handle a 20x traffic spike without degradation.
Eventual consistency: A 1-second propagation delay across regions is invisible to users. Strong consistency would add latency and reduce availability for zero perceptible benefit in a score-display system. If a user in London sees a goal 1 second after a user in New York, neither user notices or cares. This relaxed consistency is what enables the system to remain available during network partitions.
Estimate the scale of the system. Consider daily active users, read/write ratio, storage requirements, bandwidth, and any relevant QPS calculations...
Normal load: roughly 5,000 concurrent games with 50M connected viewers distributed across them. Each game generates about 200 events over its duration (scores, fouls, substitutions, timeouts). Aggregate event ingestion is roughly 1,000 events per second.
ad (Super Bowl, World Cup final): 100M+ concurrent viewers on a single game. Event rate for that game is the same (one event every few seconds), but the fan-out per event goes from thousands to 100 million.
A Super Bowl touchdown triggers a WebSocket message to every connected client. Each event message is roughly 1KB (JSON with game state, event details, timestamp). Fan-out: 100M clients x 1KB = 100GB of data delivered in approximately 2 seconds. That is roughly 400 Gbps of burst bandwidth across the entire WebSocket server fleet.
Define the APIs expected from the system. This is your chance to analyze and define the read and write paths so that you can come up with the high-level design...
This system has two distinct client types: sports data providers pushing events in, and end users consuming scores out. The API must serve both, and the ingestion side needs an idempotency key from day one, because data providers retry on timeouts and a single touchdown counted twice is unacceptable.
POST /api/events
Headers: X-API-Key: <provider_key>
Body: {
game_id: "game_456",
event_type: "score",
timestamp: "2026-02-08T22:14:33Z",
player_id: "player_789",
team_id: "team_kc",
data: { points: 6, play_type: "touchdown", yard_line: 12 },
source_event_id: "sportradar_evt_98765"
}
Response: 202 Accepted
Notes: source_event_id is the idempotency key. If this ID
has been seen before, return 200 (already processed).
POST /api/games
Headers: X-API-Key: <provider_key>
Body: {
sport_id: "nfl",
team1_id: "team_kc",
team2_id: "team_sf",
start_time: "2026-02-08T18:30:00Z",
venue: "Allegiant Stadium"
}
Response: { game_id: "game_456" }
Event types include: score, foul, substitution, timeout, period_start, period_end, injury, and correction. Each event type carries a sport-specific data payload. Score events include points and play type. Foul events include the penalty type and affected players. Period events mark transitions between quarters, halves, or innings.
GET /api/games/:game_id
Response: { game_id, sport, teams, current_score,
current_period, clock, status }
Notes: Returns current game state. Served from Redis cache.
GET /api/games/:game_id/timeline?cursor=evt_100&limit=50
Response: { events: [...], next_cursor: "evt_150" }
Notes: Cursor-based pagination for play-by-play timeline.
GET /api/games/:game_id/stats
Response: { team_stats: {...}, player_stats: [...] }
GET /api/players/:player_id/stats?season=2026
Response: { games_played, points, assists, ... }
Describe the overall system architecture. Identify the main components needed to solve the problem end-to-end. Use the diagramming tool to create a block diagram.
The architecture is an event-driven pipeline. Data flows in one direction: from sports organizations through processing and queuing, out to millions of connected clients. Every component is designed to handle failure without losing a single score update.
Separate rate limits for ingestion (data providers, authenticated via API key) and query traffic (end users, authenticated via JWT). The gateway routes ingestion requests to the Event Processing Service and query requests to the appropriate backend service. Rate limiting prevents a misbehaving provider from flooding the pipeline. The gateway also handles SSL termination, request logging, and geographic routing to direct clients to the nearest regional deployment.
Validates incoming events (does the game exist? is the event schema valid for this sport?), checks the Redis dedup set for the source_event_id, and publishes valid events to Kafka. This is the system's gatekeeper, garbage data stops here. The service is stateless and horizontally scalable. Each instance processes events independently, which means you can scale event ingestion by adding more instances behind the API Gateway's load balancer.
The architectural backbone. Topics are partitioned by game_id, which guarantees all events for a single game land on the same partition and are processed in order. Multiple independent consumer groups read from Kafka: the Database Writer persists events, WebSocket servers broadcast to clients, the Notification Service sends push alerts, and the Stats Aggregator updates player statistics. Each consumer processes at its own pace without blocking others.
The most demanding component. Each server maintains up to 50K persistent connections and an in-memory subscription map (game_id to set of connected sockets). Servers consume from Kafka and broadcast events only to clients subscribed to the relevant game. For 100M concurrent viewers, you need approximately 2,000 servers.
Consumes events from Kafka and persists them to MongoDB (event data) and PostgreSQL (game state updates like current_score, current_period, and status). Writes happen asynchronously; the writer processes at its own speed without affecting real-time delivery. It also updates the Redis cache with the latest game state after each write, ensuring that query API responses reflect recent events.
Consumes score-change events from Kafka and dispatches mobile push notifications via APNs (iOS) and FCM (Android) to users who have subscribed to specific games or teams. Batches notifications to avoid overwhelming mobile gateways during rapid scoring sequences.
Stores current game state for sub-millisecond query responses, LRU-cached timelines for active games, and deduplication keys for idempotent ingestion.
Consumes events from Kafka and computes running player and team statistics: total points, shooting percentages, time of possession, yards gained. These aggregations power the stats API endpoints and are updated incrementally as each event arrives. The aggregator writes to MongoDB and updates the Redis stats cache.
Define the data model. Identify the main entities, their attributes, and relationships. Consider the choice of database type (SQL vs NoSQL) and justify your decision based on access patterns...
Sports data splits naturally into two categories: structured reference data (teams, players, game schedules) that changes rarely, and high-velocity event data (plays, scores, stats) that arrives in bursts. Different data patterns call for different storage engines.
games
├── game_id UUID PRIMARY KEY
├── sport_id VARCHAR(20) NOT NULL
├── team1_id UUID REFERENCES teams
├── team2_id UUID REFERENCES teams
├── start_time TIMESTAMP NOT NULL
├── status ENUM('scheduled','live','final')
├── current_score JSONB
├── current_period VARCHAR(10)
└── venue VARCHAR(255)
teams
├── team_id UUID PRIMARY KEY
├── sport_id VARCHAR(20) NOT NULL
├── name VARCHAR(255) NOT NULL
└── abbreviation VARCHAR(10) NOT NULL
players
├── player_id UUID PRIMARY KEY
├── team_id UUID REFERENCES teams
├── name VARCHAR(255) NOT NULL
├── position VARCHAR(50)
└── jersey_number INTEGER
sports
├── sport_id VARCHAR(20) PRIMARY KEY
├── name VARCHAR(100) NOT NULL
└── scoring_rules JSONB
game_events collection:
{
event_id: ObjectId,
game_id: "game_456",
event_type: "score",
timestamp: ISODate("2026-02-08T22:14:33Z"),
player_id: "player_789",
team_id: "team_kc",
data: { points: 6, play_type: "touchdown",
yard_line: 12, drive_plays: 8 },
source_event_id: "sportradar_evt_98765",
sequence_number: 847
}
Variable schemas per sport are the key reason for MongoDB. A basketball event has { points: 3, shot_type: "three_pointer", distance: 28 }. A soccer event has { goal_type: "open_play", assist_player_id: "player_321" }. A baseball event has { runs: 1, rbi: 2, hit_type: "home_run", exit_velocity: 108.3 }. Adding a new sport means defining a new data shape in the event payload, no ALTER TABLE, no migration, no downtime. The game_events collection is partitioned by game_id to co-locate all events for a single game on the same shard, optimizing both writes (append to one shard) and reads (timeline query hits one shard).
Key: game:{id}:state
Value: { score, period, clock, status }
TTL: duration of game + 1 hour
Purpose: Sub-millisecond "what is the current score?" reads
Key: game:{id}:timeline
Value: ordered list of recent events
TTL: LRU eviction for idle games
Purpose: Timeline cache for active games
Key: dedup:{source_event_id}
Value: 1
TTL: 24 hours
Purpose: Idempotency check for event ingestion
PostgreSQL indexes:
games(game_id) - PK, used by every API call
games(sport_id, status) - "all live NFL games" query
games(start_time) - "upcoming games today" query
players(team_id) - "roster for team X" query
MongoDB indexes on game_events:
{ game_id: 1, sequence_number: 1 } - timeline queries
{ source_event_id: 1 } UNIQUE - backup dedup
{ game_id: 1, event_type: 1 } - filtered queries
The source_event_id unique index in MongoDB is defense-in-depth, if the Redis dedup check fails (Redis down, key expired), the database rejects the duplicate on insert.
Deep dive into 2-3 key components. Explain how they work, how they scale, discuss tradeoffs, capacity, and any relevant algorithms or data structures.
Step 1, Provider sends event: A sports data provider sends POST /api/events with the event payload including the source_event_id idempotency key.
Step 2, API Gateway authenticates: The gateway validates the API key and rate-limits the provider. Valid requests are routed to the Event Processing Service.
Step 3, Deduplication check: The Event Processing Service checks Redis for the source_event_id. If the key exists, the event is a duplicate, return 200 (already processed) and stop. If new, add the key to Redis with a 24-hour TTL.
Step 4, Validate and publish: The service validates the event schema (does the game exist? is the event type valid for this sport?) and publishes to the Kafka topic, using game_id as the partition key.
Step 5, Return 202 Accepted: The API responds before downstream processing begins. The event is durably in Kafka.
Step 6, Parallel consumption: Four independent consumer groups process the event simultaneously. The Database Writer persists to MongoDB and updates PostgreSQL game state. WebSocket servers broadcast to subscribed clients. The Notification Service queues push alerts. The Stats Aggregator updates player statistics.
Step 1, WebSocket connection: The user opens a game page. The client establishes a WebSocket connection to the nearest WebSocket server via the load balancer.
Step 2, Subscribe: The client sends { "type": "subscribe", "games": ["game_456"] }. The server adds this socket to the in-memory subscription map under game_456.
Step 3, State snapshot: The server fetches the current game state from Redis (score, period, clock) and sends it as a snapshot message. This ensures the client has the latest state without waiting for the next event.
Step 4, Real-time events: As new events arrive from Kafka, the server looks up game_456 in the subscription map and broadcasts to all subscribed sockets. The client updates the scoreboard UI.
Step 5, Unsubscribe: When the user navigates away, the client sends an unsubscribe message. The server removes the socket from the subscription map. On WebSocket close (whether intentional or due to network failure), the server cleans up all subscriptions for that socket to prevent memory leaks from abandoned connections. The cleanup is triggered by both explicit unsubscribe messages and connection close events.
he WebSocket fan-out layer is the most demanding component in this system. A single goal in a World Cup final must reach 100M connected clients within 2 seconds. This requires more than "just use WebSockets"; it requires a carefully designed subscription, routing, and broadcast architecture.
Kafka event arrives at WebSocket server, server looks up subscription map, broadcasts to all subscribed clients
Each WebSocket server handles up to 50K persistent connections. The core data structure is an in-memory map:
Map<game_id, Set<WebSocket>>
When a Kafka event arrives for game_456, the server looks up game_456 in the map, iterates the set, and sends the event JSON to each socket.
For 100M concurrent connections at 50K per server, you need approximately 2,000 servers. Every server that has at least one subscriber for a game receives that game's events from Kafka. No sticky routing is needed, unlike a chat system where messages must reach a specific user's server, here any server with subscribers for a game needs the event.
Cross-server fan-out happens naturally through Kafka consumers. Each WebSocket server runs a Kafka consumer that reads from the event topics. When a game event arrives on a partition, every server with an active consumer for that partition receives it. The Kafka consumer group assignment distributes partitions across servers, so each event is delivered to the servers that need it. This is fundamentally different from Redis pub/sub or server-to-server messaging. Kafka provides durable, ordered delivery without requiring the WebSocket servers to know about each other.