ESPNcricinfo
Tournaments, matches, innings, overs, balls, scorecards. Ball-by-ball event stream plus Observer for live-score subscribers.
Context#
ESPNcricinfo is the canonical cricket data + live-score site. Behind every match page is a model that ingests ball-by-ball events from a scorer, mutates a scorecard, computes derived statistics (run rate, partnership, required run rate, projected score, Duckworth-Lewis-Stern targets), and fans out updates to millions of concurrent viewers. The surface also includes player profiles, career statistics, head-to-head records, commentary, video highlights, news, fantasy leagues, and editorial workflow.
The HLD overlap is enormous. Live-score fan-out to ~10^7 concurrent viewers during a marquee match is a websocket / CDN-edge / pub-sub HLD problem. Statistics aggregation (career, partnership, head-to-head) is a data-warehouse / OLAP problem. Video and highlights is a media pipeline. Commentary is editorial workflow. The LLD round can model the in-process domain shape — how a Ball mutates an Innings, how the rules of T20 / ODI / Test differ, how derived statistics are computed from the event stream — and name the seams to everything else.
The interviewer’s hidden objectives, in order:
- Can you declare scope first and cut decisively? Live video, fantasy, editorial workflow are the canonical cuts.
- Can you identify the six core aggregates — Tournament, Match, Innings, Over, Ball, ScoreCard — and the event-stream relationship between Ball and ScoreCard?
- Can you model three match types (T20, ODI, Test) as a Strategy on rules rather than three separate classes with
instanceofchains? - Can you draw the ball-by-ball event flow — Scorer adds Ball; Ball mutates Innings; Innings notifies Observers (live-score subscribers, commentary, statistics)?
- Can you handle special balls — wide, no-ball, bye, leg-bye, wicket, dead ball — without an
if-chain on outcome type? - Can you defend derived data: career statistics, partnerships, head-to-head are not stored, they’re computed.
Requirements (functional and non-functional)#
Scope is the most points-bearing decision. The cut below fits a 45-minute round.
Functional — in scope.
- A
Tournament(e.g., IPL 2026, World Cup) contains manyMatches between twoTeams. Each match has akind(T20, ODI, Test) that drives rule differences. - A
Matchhas twoInningsfor limited-overs cricket; up to four for a Test. AnInningsbelongs to a battingTeam; the other team bowls. - An
Inningsis a sequence ofOvers; eachOveris a sequence ofBalls. Balls carry outcome: runs (0-6), wicket type, extras (wide, no-ball, bye, leg-bye), and per-ball metadata (batter on strike, bowler, fielder if applicable). - A
Scoreruser (one per match) appendsBalls to the currentInnings. EachBallmutates theScoreCard(team total, wickets, batter scores, bowler figures, partnership) and fires events to subscribers. ScoreCardis the read-projection of the event stream — derived, not the source of truth. Replaying the balls reproduces the scorecard.- Live-score viewers subscribe to a match; updates push to them via Observer.
- The system computes statistics: career batting / bowling averages, partnership scores, head-to-head records — all derived from the ball event stream.
Functional — out of scope (called out explicitly).
- Live video streaming and highlights pipeline. A separate media system entirely.
- Editorial commentary writing tools. A
Commentaryaggregate is named; the CMS is out. - Fantasy cricket / dream-team leagues. Its own product.
- Predictive analytics (win probability, projected score) — the model carries the event stream; algorithms are a separate concern.
- Duckworth-Lewis-Stern target calculation. A
TargetStrategyseam is named; the algorithm is a black box. - User auth, profiles, comments, social. Out.
- Multi-language commentary. One language stream; translation is downstream.
- Player profile editorial content (biographies, photos). The
Playeraggregate exists with identity; the editorial layer is out. - Push-notification infrastructure (FCM, APNS). Observer fires events in-process; the push channel is HLD.
Reciting these cuts on the whiteboard first is the high-altitude move.
Non-functional.
- The model should support
~10^7concurrent live-score viewers per marquee match. OOD optimises only the in-process shape — the fan-out network layer is HLD. - Ball updates are append-only. A correction (the scorer made a mistake) is a separate “amend” command — the event log records both.
- Derived statistics are eventually computed from the event stream — they’re never the source of truth.
- Match / Innings state changes are observable.
Use case diagram#
┌────────────────┐ │ Scorer │ └────────┬───────┘ │ [add ball] │ ▼ ┌─────────────────────────────────────────────────────────┐ │ ESPNcricinfo Match System │ └─────┬───────────────┬─────────────────┬─────────────────┘ ▲ ▲ ▲ │ │ │ ┌───────────┐ ┌──────────────┐ ┌────────────┐ │ Viewer │ │ Statistics │ │ Commentator│ ├───────────┤ ├──────────────┤ ├────────────┤ │ subscribe │ │ subscribe │ │ subscribe │ │ live score│ │ recompute │ │ + write │ │ │ │ aggregates │ │ │ └───────────┘ └──────────────┘ └────────────┘
┌────────────────┐ │ Editor │── (cut: editorial CMS) └────────────────┘Four actors: the scorer (single source of truth for ball events), the viewer (subscribes to live scores), the statistics service (subscribes to recompute aggregates), and the commentator (subscribes to add commentary alongside the ball stream). Editorial content is cut.
Class diagram#
┌────────────────────────┐ ┌────────────────────────┐ │ Tournament │ │ Team │ ├────────────────────────┤ 1 * ├────────────────────────┤ │ id, name, format ├────────►│ id, name, players │ │ matches : List │ └────────────────────────┘ └─────────┬──────────────┘ ▲ 2 │ 1 │ │ * │ ▼ │ ┌────────────────────────┐ │ │ Match │──────────────────┘ ├────────────────────────┤ │ id, kind : MatchKind │ │ teams : (Team, Team) │ │ innings : List<Innings>│ │ status : MatchStatus │ ┌────────────────────────┐ │ rules : MatchRules │─uses───►│ MatchRules │◁── T20Rules, │ scoreCard : ScoreCard │ ├────────────────────────┤ ODIRules, │ commentary : List │ │ oversPerInnings() │ TestRules │ result() │ │ inningsPerSide() │ └─────────┬──────────────┘ │ isMatchOver(state) │ │ 1 │ isInningsOver(state) │ │ * └────────────────────────┘ ▼ ┌────────────────────────┐ │ Innings │ ├────────────────────────┤ │ battingTeam, bowlingTeam│ │ overs : List<Over> │ │ totalRuns, wickets │ │ status : InningsStatus │ │ append(Ball) │ └─────────┬──────────────┘ │ 1 │ * ▼ ┌────────────────────────┐ ┌────────────────────────┐ │ Over │ │ Ball │ ├────────────────────────┤ 1 * ├────────────────────────┤ │ number ├────────►│ outcome : BallOutcome │ │ bowler │ │ batter, bowler, fielder│ │ balls : List<Ball> │ │ runs, isExtra, isWicket│ │ isComplete() │ │ extraType, wicketType │ └────────────────────────┘ └────────────────────────┘
┌────────────────────────┐ ┌────────────────────────┐ │ ScoreCard │ │ LiveScoreObserver │◁── Viewer, ├────────────────────────┤◄────────┤────────────────────────┤ Statistics, │ derived from event log │ notify │ onBall(BallEvent) │ Commentary │ totals, partnerships, │ │ onInningsEnd() │ │ batter / bowler figures│ │ onMatchEnd() │ │ replay(events) │ └────────────────────────┘ └────────────────────────┘Four patterns are doing the load-bearing work:
- Observer pattern is the spine. Ball events fan out to viewers, statistics, and commentary. The match is the publisher; subscribers register and unregister freely.
- Strategy pattern on
MatchRules. T20, ODI, Test differ inoversPerInnings,inningsPerSide, and completion criteria. TheMatchdoesn’t care about format; it callsrules.isMatchOver(state)andrules.isInningsOver(state). - Event-sourcing flavour on the relationship between
BallandScoreCard. TheList<Ball>is the source of truth;ScoreCardis a derived view computed by replay. Corrections amend the event log; the view recomputes. - State pattern on
InningsStatus(InProgress -> AllOut | OversComplete | Declared) andMatchStatus(Scheduled -> InProgress -> Completed | Abandoned | NoResult).
What is deliberately not in the diagram:
- No
BallOutcomegod enum. The ball carries multiple orthogonal flags (runs,isWicket,isExtra,extraType,wicketType) — encoding all combinations as one enum (SIX_NO_BALL_PLUS_FOUR_RUNS) would explode. The flags are independent. - No three-class hierarchy
T20Match / ODIMatch / TestMatch. Rules vary; the entity does not.MatchRulesis the volatile axis;Matchis stable. - No
Career/Statisticsaggregate holding pre-computed numbers. Statistics is a listener that maintains its own indices.
Sequence diagram (key flows)#
Ball appended, the event flow:
Scorer Match Innings Over Ball ScoreCard Observer(viewer) Observer(stats) │ addBall(b)│ │ │ │ │ │ │ │──────────►│ │ │ │ │ │ │ │ │ append(b) │ │ │ │ │ │ │───────►│ │ │ │ │ │ │ │ │ currentOver.add(b) │ │ │ │ │ │───────►│ │ │ │ │ │ │ │ if not extra && b.legal: │ │ │ │ │ │ over advances ball count │ │ │ │ │ │ if 6 legal balls bowled: over ends │ │ │ │ │ │ update innings totals (runs, wickets) │ │ │ │ │ │ scoreCard.apply(b) │ │ │ │ │ │──────────────────────────►│ │ │ │ │ rules.isInningsOver(state)? │ │ │ │ │ if yes: innings.close(); maybe match end checks │ │ │ │ │ fire BallEvent to all subscribers (per-match list) │ │ │──────────────────────────────────────────────────►│ │ │ │ ─────────────────────────────────────────────────────────────────►│ │ │ ack │ │◄──────────│ │Subscribe / unsubscribe:
Viewer Match │ subscribe(viewerObs) │─────────────────────►│ │ add to listeners │ │◄─────────────────────│ │ (now receives ball events) │ │ ... viewer closes tab ... │ unsubscribe(viewerObs) │─────────────────────►│ │ remove from listeners │◄─────────────────────│Innings transitions on the second-innings chase (T20 / ODI), the most rule-heavy moment:
Innings MatchRules Match │ ball appended; runs added to total │ rules.isInningsOver(state)? │──────────────────────────►│ │ T20 rules check: │ allOut? OR overs == 20? OR │ chase: totalRuns > targetRuns? (chasing 2nd innings only) │◄──────────────────────────│ │ true => close innings │ rules.isMatchOver(state)? │ │───────────────────────────►│ │ 2nd innings ended; result decidable │◄───────────────────────────│ │ match.status = Completed; notify observersActivity diagram (for non-trivial state)#
The Innings lifecycle for a limited-overs match (T20 / ODI):
┌─────────────┐ │ InProgress │── all out (10 wickets) ────►┌────────────┐ └──────┬──────┘ │ AllOut │ (terminal) │ └────────────┘ │ overs completed (T20: 20, ODI: 50) ▼ ┌─────────────┐ │ OversComplete│ (terminal) └─────────────┘ │ │ chase: target met (2nd innings only) ▼ ┌─────────────┐ │ TargetReached│ (terminal, only 2nd innings of LO) └─────────────┘The Innings lifecycle for a Test adds Declared:
┌─────────────┐ │ InProgress │── all out ────►┌──────────┐ └──────┬──────┘ │ AllOut │ │ └──────────┘ │ captain declares ▼ ┌─────────────┐ │ Declared │ (terminal) └─────────────┘The Match lifecycle:
┌─────────────┐ │ Scheduled │── start ─────►┌──────────────┐ └─────────────┘ │ InProgress │── rain / abandon ──►┌──────────────┐ └──────┬───────┘ │ Abandoned │ │ rules.isMatchOver() └──────────────┘ ▼ ┌──────────────┐ │ Completed │ (terminal) └──────────────┘Java implementation#
A representative slice: the ball event flow, the MatchRules strategy, the Innings append path, and the Observer wiring.
public enum MatchKind { T20, ODI, TEST }public enum InningsStatus { IN_PROGRESS, ALL_OUT, OVERS_COMPLETE, TARGET_REACHED, DECLARED }public enum MatchStatus { SCHEDULED, IN_PROGRESS, COMPLETED, ABANDONED, NO_RESULT }public enum ExtraType { NONE, WIDE, NO_BALL, BYE, LEG_BYE }public enum WicketType { NONE, BOWLED, CAUGHT, LBW, RUN_OUT, STUMPED, HIT_WICKET }
public final class Ball { final Player batter; final Player bowler; final Player fielder; // nullable final int runs; // 0..6 final ExtraType extra; final WicketType wicket; final boolean isLegalDelivery; // wide / no-ball don't count toward the over
private Ball(Builder b) { this.batter = b.batter; this.bowler = b.bowler; this.fielder = b.fielder; this.runs = b.runs; this.extra = b.extra; this.wicket = b.wicket; this.isLegalDelivery = (b.extra != ExtraType.WIDE && b.extra != ExtraType.NO_BALL); }
public boolean isWicket() { return wicket != WicketType.NONE; } public boolean isExtra() { return extra != ExtraType.NONE; }
public static class Builder { /* … */ public Ball build() { return new Ball(this); } }}
public interface MatchRules { int oversPerInnings(); int inningsPerSide(); boolean isInningsOver(InningsSnapshot s); boolean isMatchOver(MatchSnapshot s);}
public final class T20Rules implements MatchRules { public int oversPerInnings() { return 20; } public int inningsPerSide() { return 1; } public boolean isInningsOver(InningsSnapshot s) { if (s.wickets >= 10) return true; if (s.completedOvers >= 20) return true; // 2nd innings: chase succeeded if (s.inningsIndex == 1 && s.totalRuns > s.targetRuns) return true; return false; } public boolean isMatchOver(MatchSnapshot s) { return s.completedInnings.size() == 2; }}
public final class TestRules implements MatchRules { public int oversPerInnings() { return Integer.MAX_VALUE; } public int inningsPerSide() { return 2; } public boolean isInningsOver(InningsSnapshot s) { return s.wickets >= 10 || s.declared; } public boolean isMatchOver(MatchSnapshot s) { // Day-5-end logic, follow-on, etc., elided. return s.completedInnings.size() == 4 || s.allInningsDecided(); }}
public final class Innings { private final Team battingTeam; private final Team bowlingTeam; private final List<Over> overs = new ArrayList<>(); private final int inningsIndex; // 0-based private int totalRuns = 0; private int wickets = 0; private boolean declared = false; private InningsStatus status = InningsStatus.IN_PROGRESS;
public Innings(Team bat, Team bowl, int idx) { this.battingTeam = bat; this.bowlingTeam = bowl; this.inningsIndex = idx; overs.add(new Over(0)); }
public synchronized void append(Ball b, MatchRules rules, int targetRuns) { if (status != InningsStatus.IN_PROGRESS) { throw new IllegalStateException("innings closed"); } Over current = overs.get(overs.size() - 1); current.add(b); totalRuns += b.runs + (b.isExtra() ? 1 : 0); if (b.isWicket() && b.wicket != WicketType.RUN_OUT) wickets++; else if (b.wicket == WicketType.RUN_OUT) wickets++;
// Move to a new over after 6 legal deliveries. if (current.legalDeliveries() == 6) { overs.add(new Over(overs.size())); }
InningsSnapshot snap = snapshot(targetRuns); if (rules.isInningsOver(snap)) close(deriveTerminalStatus(snap, rules)); }
public void declare() { this.declared = true; this.status = InningsStatus.DECLARED; }
private void close(InningsStatus terminal) { this.status = terminal; } public InningsSnapshot snapshot(int targetRuns) { /* … */ return null; } private InningsStatus deriveTerminalStatus(InningsSnapshot s, MatchRules r) { /* … */ return null; } public InningsStatus status() { return status; }}
public interface MatchObserver { default void onBall(Match m, Ball b) {} default void onInningsEnd(Match m, Innings i) {} default void onMatchEnd(Match m) {}}
public final class Match { private final String id; private final MatchKind kind; private final MatchRules rules; private final List<Innings> innings = new ArrayList<>(); private MatchStatus status = MatchStatus.SCHEDULED; private final List<MatchObserver> observers = new CopyOnWriteArrayList<>();
public Match(String id, MatchKind kind, MatchRules rules) { this.id = id; this.kind = kind; this.rules = rules; }
public void subscribe(MatchObserver o) { observers.add(o); } public void unsubscribe(MatchObserver o) { observers.remove(o); }
public synchronized void addBall(Ball b) { if (status == MatchStatus.SCHEDULED) status = MatchStatus.IN_PROGRESS; Innings current = innings.get(innings.size() - 1); int target = (innings.size() == 2) ? innings.get(0).snapshot(0).totalRuns + 1 : 0;
current.append(b, rules, target); fire(o -> o.onBall(this, b));
if (current.status() != InningsStatus.IN_PROGRESS) { fire(o -> o.onInningsEnd(this, current)); if (rules.isMatchOver(snapshot())) { status = MatchStatus.COMPLETED; fire(o -> o.onMatchEnd(this)); } else { // Start the next innings (sides swap in LO; sides swap by index in Test). innings.add(nextInnings(current)); } } }
private MatchSnapshot snapshot() { /* … */ return null; } private Innings nextInnings(Innings prev) { /* … */ return null; }
private void fire(java.util.function.Consumer<MatchObserver> fn) { for (MatchObserver o : observers) { try { fn.accept(o); } catch (RuntimeException ignored) { /* log */ } } }}
/** A live-score viewer subscriber. */public final class LiveScoreSubscriber implements MatchObserver { private final String connectionId; public LiveScoreSubscriber(String c) { this.connectionId = c; } public void onBall(Match m, Ball b) { // Push a JSON delta over the connection (out of OOD scope). }}
/** A statistics maintainer that updates career / partnership indices. */public final class StatisticsSubscriber implements MatchObserver { public void onBall(Match m, Ball b) { // Update batter runs, bowler figures, partnership. // (Persistence is a seam.) }}Notes the interviewer will look for:
- Ball is immutable. Append-only; corrections are a separate command (an “amend ball” event in the log, not a mutation).
MatchRulesis the volatile axis. TheMatchandInningsdon’t know format-specific numbers; they askrules.- Subscribers register on the match, not on innings. Innings come and go; the subscription lives at the match level.
CopyOnWriteArrayListfor observers. Reads dominate during a live match (every ball iterates the list); writes (sub / unsub) are rare. The standard Observer-with-CoW pattern.- A failing subscriber doesn’t break others. The
try / catchper observer infireis the classic Observer-robustness trick. - Innings switching happens inside
addBallwhenisInningsOverreturns true. The match coordinates; the innings doesn’t know about the next innings. - Run-out vs caught vs bowled — wicket increments uniformly; the type lives on the
Ballfor downstream (commentary, statistics) consumption.
Trade-offs and extensions#
Doubles as the what-I-cut list — recite at the end of the round.
| Decision | Why | Cost / extension shape |
|---|---|---|
| Ball event stream is source of truth | Enables replay, corrections, derived views. | Storage shape: append-only event log per match; scorecard is materialised on read or maintained eagerly by a subscriber. |
ScoreCard as derived view | Avoids the “scorecard is the model” trap where corrections become brittle. | A live subscriber maintains the in-memory scorecard; a persistent subscriber writes to a denormalised store for the website. |
MatchRules Strategy, not subclass hierarchy | T20 / ODI / Test differ in numbers and completion criteria, not entity identity. | New formats (T10, The Hundred) add a new MatchRules; no entity change. |
Observer with CopyOnWriteArrayList | Reads dominate; writes rare. | At ~10x subscriber load, sub / unsub becomes a hot path; shard observers (per region, per shard key). |
| Subscribers in-process | OOD scope. | At ESPN scale, each subscriber is a websocket session on an edge server; the in-process Observer becomes a publish to a pub-sub (Redis, Kafka, NATS) that edge servers consume. |
| Wicket / extra / runs as orthogonal flags | Avoids enum explosion. | Adding a new outcome (e.g., penalty runs) adds a flag, not a class. |
| Statistics as a subscriber | Decouples aggregation from match flow. | A real ESPN has multiple aggregation pipelines (live, end-of-match, career, season). Each is its own subscriber. |
| Single language commentary | OOD scope. | Commentary becomes Map<Language, List<Comment>>; translation pipeline is its own service. |
| No video / highlights | Media pipeline is its own system. | Highlights service subscribes to balls flagged with isHighlight; produces clips out-of-band. |
| Innings switching in-match | Match coordinates lifecycle. | Test follow-on rule adds a branch — the match consults rules.shouldEnforceFollowOn(state) before constructing the next innings. |
| Append-only events | Replay safety. | Corrections are AmendBallEvent records that the replay processor knows to apply over the original ball. |
Scale breakpoints (where the design must change):
- At
~10^4concurrent viewers per match, in-process Observer is fine. At~10^7(marquee Indian / global match), Observer is replaced by a publish to a fan-out tier (websocket gateway on edge); theMatchaggregate publishes one message per ball; the gateway broadcasts. - At career-statistics scale, naive recomputation by replaying every ball ever bowled is infeasible. The aggregation subscriber maintains incremental indices keyed by player; recomputation happens for corrections only and is targeted.
- At tournament scale, derived data (run rate over last 5 overs, projected score, manhattan / worm charts) is computed by dedicated subscribers — the model carries the raw events and the seams.
Likely follow-up extensions:
- Powerplay tracking. The first 6 overs in T20 (and first 10 in ODI) are powerplay; field-restriction rules change. Add a
PowerplayInfoderived field on the scorecard; computed by a subscriber. - Super over. Tied T20 / ODI matches go to a super over. A
SuperOveris just anotherInningswithoversPerInnings = 1andmaxWickets = 2—MatchRulesdecides when to enter. - DLS target adjustment. Rain-shortened chases need a new target. A
TargetStrategyrecomputes; theMatchconsumes the updated target on the nextisInningsOvercheck. - Player swap on injury. A substitute / concussion replacement updates the team roster mid-match; the model needs
RosterEvents in the same event log. - Multi-language commentary. Commentary becomes a
Map<Language, List<Comment>>; translation subscribers consume the English stream and emit translated entries. - Replay for video sync. Every ball carries a timestamp; the video pipeline aligns clips to ball boundaries.
Mock interview follow-ups#
- “Why is scorecard derived and not the source of truth?” — Corrections. If the scorer mistypes a six as a four, fixing the source ball and recomputing produces the right scorecard. If scorecard is the source, the correction is a delta that can’t replay cleanly. Event-sourced flavour at the right altitude.
- “How do you handle a wide that also yields runs?” — Ball flags are orthogonal:
extra = WIDE,runs = 2,isLegalDelivery = false. The over advances ball count only on legal deliveries. The scorecard addsruns + 1(the wide penalty) to the team total. - “What’s the state machine on
Innings?” — Show the diagram.InProgress -> AllOut | OversComplete | TargetReachedfor limited-overs; addsDeclaredfor Test. The terminal state varies byMatchRules. - “How do T20 and Test differ in code?” —
T20RulesvsTestRules— twoMatchRulesimplementations. The match doesn’t know which it is; it callsrules.isInningsOver(state). Adding T10 or The Hundred is a newMatchRules, no entity change. - “
10^7viewers per match — does Observer work?” — In-process Observer doesn’t scale that far. The model publishes oneBallEventper ball to a pub-sub tier; an edge websocket fleet subscribes and fans out. The OOD shape is unchanged; the Observer becomes a publish. - “A scorer makes a mistake on ball 12. How is it corrected?” — An
AmendBallEventis appended pointing at the original ball’s id. The scorecard subscriber re-applies events from the amended ball onward. Statistics subscribers do the same. The original ball stays in the log for audit. - “What did you cut and why?” — Recite the table. Video, fantasy, editorial, DLS algorithm, push-notification infrastructure, predictive analytics. Each is its own product.
Related#
- Facebook — same Observer-at-scale fan-out shape with social-feed semantics instead of ball events.
- LinkedIn — derived-feed-from-events sibling.
- Observer Pattern — live-score subscription is the textbook use case.
- Strategy Pattern —
MatchRulescarves out format-specific volatility. - State Pattern —
InningsandMatchboth run state machines.