ESPNcricinfo

Tournaments, matches, innings, overs, balls, scorecards. Ball-by-ball event stream plus Observer for live-score subscribers.

System Advanced
18 min read
ood case-study cricket observer-pattern strategy-pattern

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 instanceof chains?
  • 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 many Matches between two Teams. Each match has a kind (T20, ODI, Test) that drives rule differences.
  • A Match has two Innings for limited-overs cricket; up to four for a Test. An Innings belongs to a batting Team; the other team bowls.
  • An Innings is a sequence of Overs; each Over is a sequence of Balls. 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 Scorer user (one per match) appends Balls to the current Innings. Each Ball mutates the ScoreCard (team total, wickets, batter scores, bowler figures, partnership) and fires events to subscribers.
  • ScoreCard is 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 Commentary aggregate 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 TargetStrategy seam 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 Player aggregate 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^7 concurrent 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 in oversPerInnings, inningsPerSide, and completion criteria. The Match doesn’t care about format; it calls rules.isMatchOver(state) and rules.isInningsOver(state).
  • Event-sourcing flavour on the relationship between Ball and ScoreCard. The List<Ball> is the source of truth; ScoreCard is a derived view computed by replay. Corrections amend the event log; the view recomputes.
  • State pattern on InningsStatus (InProgress -> AllOut | OversComplete | Declared) and MatchStatus (Scheduled -> InProgress -> Completed | Abandoned | NoResult).

What is deliberately not in the diagram:

  • No BallOutcome god 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. MatchRules is the volatile axis; Match is stable.
  • No Career / Statistics aggregate 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 observers

Activity 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).
  • MatchRules is the volatile axis. The Match and Innings don’t know format-specific numbers; they ask rules.
  • Subscribers register on the match, not on innings. Innings come and go; the subscription lives at the match level.
  • CopyOnWriteArrayList for 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 / catch per observer in fire is the classic Observer-robustness trick.
  • Innings switching happens inside addBall when isInningsOver returns 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 Ball for downstream (commentary, statistics) consumption.

Trade-offs and extensions#

Doubles as the what-I-cut list — recite at the end of the round.

DecisionWhyCost / extension shape
Ball event stream is source of truthEnables 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 viewAvoids 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 hierarchyT20 / 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 CopyOnWriteArrayListReads dominate; writes rare.At ~10x subscriber load, sub / unsub becomes a hot path; shard observers (per region, per shard key).
Subscribers in-processOOD 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 flagsAvoids enum explosion.Adding a new outcome (e.g., penalty runs) adds a flag, not a class.
Statistics as a subscriberDecouples aggregation from match flow.A real ESPN has multiple aggregation pipelines (live, end-of-match, career, season). Each is its own subscriber.
Single language commentaryOOD scope.Commentary becomes Map<Language, List<Comment>>; translation pipeline is its own service.
No video / highlightsMedia pipeline is its own system.Highlights service subscribes to balls flagged with isHighlight; produces clips out-of-band.
Innings switching in-matchMatch coordinates lifecycle.Test follow-on rule adds a branch — the match consults rules.shouldEnforceFollowOn(state) before constructing the next innings.
Append-only eventsReplay 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^4 concurrent 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); the Match aggregate 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 PowerplayInfo derived field on the scorecard; computed by a subscriber.
  • Super over. Tied T20 / ODI matches go to a super over. A SuperOver is just another Innings with oversPerInnings = 1 and maxWickets = 2MatchRules decides when to enter.
  • DLS target adjustment. Rain-shortened chases need a new target. A TargetStrategy recomputes; the Match consumes the updated target on the next isInningsOver check.
  • 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 adds runs + 1 (the wide penalty) to the team total.
  • “What’s the state machine on Innings?” — Show the diagram. InProgress -> AllOut | OversComplete | TargetReached for limited-overs; adds Declared for Test. The terminal state varies by MatchRules.
  • “How do T20 and Test differ in code?”T20Rules vs TestRules — two MatchRules implementations. The match doesn’t know which it is; it calls rules.isInningsOver(state). Adding T10 or The Hundred is a new MatchRules, no entity change.
  • 10^7 viewers per match — does Observer work?” — In-process Observer doesn’t scale that far. The model publishes one BallEvent per 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 AmendBallEvent is 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.
  • 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 PatternMatchRules carves out format-specific volatility.
  • State PatternInnings and Match both run state machines.
Search ESC

Keyboard shortcuts

Shortcuts are disabled while typing in inputs.