Back to Machine Coding
Machine Coding
easy
mid

How would you design and build a cricket scoreboard UI (low level design)?

Domain: `Match`, `Innings`, `Over`, `Ball`, `Batter`, `Bowler`. Each ball updates running totals (runs, wickets, balls bowled) and triggers UI updates. State machine: ball-by-ball → over-completed → innings-end → match-end. Optimistic updates from a real-time feed (WebSocket); recompute derived stats (run rate, required rate) from primary ball events. Tests are easy because logic is pure.

6 min read·~40 min to think through

A cricket scoreboard LLD is a great domain-modeling question: lots of natural objects, interesting state transitions, and pure derivations that make testing easy.

1. Domain model

ts
Match
 ├── teams: [Team, Team]
 ├── format: T20 | ODI | Test
 ├── innings: [Innings, Innings] (or 4 for Tests)
 └── currentInningsIndex

Innings
 ├── battingTeam, bowlingTeam
 ├── overs: Over[]
 ├── runs, wickets, balls, extras
 ├── batters: { [batterId]: BatterStats }
 ├── bowlers: { [bowlerId]: BowlerStats }
 └── striker, nonStriker

Over
 ├── number, bowler
 └── balls: Ball[]

Ball
 ├── runs (off bat)
 ├── extras: { wide, noBall, bye, legBye, penalty }
 ├── wicket?: { type, batterOut, fielder? }
 ├── isLegal: boolean (false for wide / no-ball)
 └── striker, nonStriker (at this ball)

BatterStats { runs, balls, fours, sixes, isOut }
BowlerStats { overs, runs, wickets, dotBalls, maidens }

2. State machine

ts
Match: NotStarted → InProgress → Completed
Innings: NotStarted → InProgress → Ended (declared / all-out / overs-up / target)
Over: InProgress → Ended (6 legal balls)
Ball: pending → recorded (final)

State transitions are triggered by:

  • Recording a ball.
  • Wicket falling → batter swap.
  • Over end → bowler swap, strike change.
  • Innings end → switch teams.

3. Recording a ball

js
function recordBall(innings, ball) {
  const runs = ball.runs + sumExtras(ball.extras);
  innings.runs += runs;
  innings.extras += sumExtras(ball.extras);
  if (ball.isLegal) innings.balls++;
  if (ball.wicket) {
    innings.wickets++;
    innings.batters[ball.wicket.batterOut].isOut = true;
    // ... fall-of-wicket entry
  }

  // batter stats
  const striker = innings.batters[innings.striker];
  if (ball.isLegal) striker.balls++;
  striker.runs += ball.runs;
  if (ball.runs === 4) striker.fours++;
  if (ball.runs === 6) striker.sixes++;

  // bowler stats
  const bowler = innings.bowlers[currentBowlerId];
  bowler.runs += runs;
  if (ball.isLegal) bowler.balls++;       // overs computed from balls
  if (ball.wicket && ball.wicket.type !== "runOut") bowler.wickets++;
  if (ball.runs === 0 && ball.isLegal && !ball.wicket) bowler.dotBalls++;

  // strike change (odd runs off bat, or end of over)
  if (ball.runs % 2 === 1) swapStrike(innings);
  if (innings.balls % 6 === 0 && ball.isLegal) {
    swapStrike(innings);
    // trigger new over, new bowler
  }
}

Every ball is a small set of pure mutations. Easy to unit test.

4. Derived stats (computed, never stored)

  • Run rate = runs / overs (overs = legal balls / 6).
  • Required run rate = (target - runs) / oversRemaining.
  • Partnership = sum of runs since last wicket.
  • Powerplay status = derived from format + current over.

Compute these from primary data; never store them — they'd drift.

5. UI layout

ts
┌─────────────────────────────────────┐
│  India 178/4 (16.3)  vs  Aus 162/8
Target: 163 (Need 28 in 3.3)       │
├─────────────────────────────────────┤
│  Kohli*    62 (45)  4×5  6×3         │ ← striker (* marker)
│  Rohit     34 (28)                   │
├─────────────────────────────────────┤
│  Bumrah    3.3-0-22-1                │ ← current bowler
├─────────────────────────────────────┤
│  This over: 1 4 W 2 1                │ ← ball-by-ball
└─────────────────────────────────────┘
  • Big top: score, overs, target.
  • Mid: current batters with bold strike marker.
  • Bowler stats.
  • Recent balls (last 6 or current over) — keys, dots, boundaries, wicket.
  • Optional: live updates pulse on the changed cells.

6. Real-time updates

  • WebSocket feed of ball events.
  • Optimistic: append to local model; rollback if server rejects.
  • Show "Live" indicator; reconnect on drop.
  • Replay missed balls by event id since last seen.

7. Animations

  • Pulse the changed score on update (subtle).
  • Wicket triggers a brief overlay ("WICKET!").
  • Boundary highlights the ball.
  • Respect prefers-reduced-motion.

8. Edge cases

  • Free hit after a no-ball.
  • Retired hurt — batter not out but no longer batting.
  • DRS review pending — show "Under review" pill.
  • Rain delays / D/L — derived target changes.
  • Super over — handle extra-innings logic.

9. Accessibility

  • Score updates announced via a polite live region ("KOHLI 4 — India 182 for 4").
  • Don't rely on color for boundaries — text/icons.
  • Keyboard: tab through batters/bowlers for stats panels.

10. Testing

Because the core (recordBall, derive run rate) is pure, write unit tests for:

  • Specific ball patterns (wide + boundary, no-ball + 4 → 5 + free hit next).
  • Strike-change rules.
  • Over completion.
  • Innings-end conditions per format.

Interview framing

"Core domain is Match → Innings[] → Over[] → Ball[], with running stats on batters and bowlers. Recording a ball is a small set of pure mutations: increment innings runs, update batter stats, update bowler stats, swap strike on odd runs or over-end, transition state on innings/match end. Derived stats — run rate, required rate, partnership — are computed, never stored. Real-time updates over WebSocket with optimistic appending and event-id-based catch-up on reconnect. UI: big score top, batters with strike marker, bowler, this-over ball-by-ball. Edge cases: free hit, retired hurt, DRS, D/L, super over. Because the logic is pure, unit testing is easy — that's the part to call out for senior signal."

Follow-up questions

  • Walk through recording a no-ball followed by a 4.
  • How do you handle a strike change?
  • Why compute run rate instead of storing it?
  • What's your strategy for sync on reconnect?

Common mistakes

  • Storing derived stats and drifting out of sync.
  • Not handling free hit / no-ball correctly.
  • Auto-updating UI without optimistic rollback.
  • Coloring boundaries/wickets without text/icon backup.

Performance considerations

  • Small data; perf is irrelevant. Memoize derived stats; subscribe to event stream by match id.

Edge cases

  • Free hit, retired hurt, DRS, D/L, super over.
  • Innings declared mid-over (Tests).
  • Run-out where bowler doesn't get the wicket credit.

Real-world examples

  • Cricinfo, Cricbuzz live scorecards.

Senior engineer discussion

Seniors model the domain cleanly, keep derived stats pure, separate state machine from view, and call out testability as a deliberate design outcome — not a fluke.

Related questions