Design Cricinfo
Design Cricinfo
Cricinfo is a sports site dedicated to cricket. It carries live ball-by-ball commentary of matches, a searchable archive of every historical match, and news and articles. The hard part of the model is not the news feed — it is faithfully recording the state of a cricket match so that statistics, commentary, and results all fall out of the same source of truth.
We will design the domain model around that recording problem, then layer stats and queries on top.
Requirements
- Track all cricket-playing teams and their matches.
- Show live ball-by-ball commentary of matches.
- All international cricket rules must be followed — scoring, extras, and dismissals are attributed exactly as the laws of cricket prescribe.
- A team entering a tournament announces a squad (a set of players).
- For each match, both teams pick a playing eleven from that squad.
- Record stats for players, matches, and tournaments.
- Answer global stat queries such as “Who is the highest wicket-taker of all time?” or “Who has scored the most centuries in Tests?”
- Support ODI, Test, and T20 matches.
Actors
- Admin — adds and edits players, teams, tournaments, matches; records ball-by-ball details.
- Commentator — attaches ball-by-ball commentary to each delivery.
Core domain classes
The recording model is a strict composition chain: a Match has many Innings, each Innings has many Overs, and each Over has the Balls bowled in it. A Ball carries a list of Runs (a single delivery can yield several runs of different kinds), an optional Wicket, and a Commentary.
Because aggregate computations — innings total, wickets fallen, a bowler's runs conceded — are folds over the balls, the leaf classes must expose their balls to callers. The original sketch omitted a getter on Over, which made any code that folded over balls (including Innings.wickets()) fail to compile. The fix is an unmodifiable accessor so callers can read the balls without being able to mutate the over's internal list.
Enums
public enum MatchFormat { ODI, T20, TEST }
public enum MatchResult { LIVE, FINISHED, DRAWN, CANCELED }
public enum UmpireType { FIELD, RESERVED, TV }
public enum WicketType {
BOWLED, CAUGHT, STUMPED, RUN_OUT,
LBW, RETIRED_HURT, HIT_WICKET, OBSTRUCTING
}
// BallType classifies the delivery itself.
public enum BallType { NORMAL, WIDE, NO_BALL }
// RunType classifies how each individual run was scored.
// Note: WIDE and NO_BALL extras are recorded as runs here so the
// scorecard can separate extras from runs off the bat.
public enum RunType {
NORMAL, FOUR, SIX, LEG_BYE, BYE, WIDE, NO_BALL
}Scoring note. There is no OVERTHROW run type, and that is deliberate. An overthrow is not its own category of run — it is extra runs scored off the same delivery after a misfield, and the laws attribute those extra runs to whatever the delivery already was. On a wide, the penalty run and any further runs from a misfield are all wides; on a legal ball they are runs off the bat (or byes/leg-byes). Encoding overthrows as a distinct RunType would double-count or mis-attribute extras and break the scorecard.
No-ball and the free hit. A NO_BALL carries its own one-run penalty and grants a free hit on the next legal delivery — model that as a freeHit flag set on the following Ball, on which the batter cannot be dismissed except by run-out (or the handful of non-delivery dismissals). The rules engine reads that flag when attributing a Wicket, so a "bowled" on a free-hit ball records runs but no wicket.
Over and Ball
public class Over {
private int number;
private final List<Ball> balls = new ArrayList<>();
public boolean addBall(Ball ball) { return balls.add(ball); }
// REQUIRED accessor: callers fold over the balls to compute
// innings totals, wickets, and per-bowler figures. Returning an
// unmodifiable view lets them read without mutating our list.
public List<Ball> balls() {
return Collections.unmodifiableList(balls);
}
// A legal ball is anything that is not a wide or a no-ball.
public long legalBallCount() {
return balls.stream()
.filter(b -> b.type() == BallType.NORMAL)
.count();
}
public boolean isComplete() { return legalBallCount() >= 6; }
public int runs() {
return balls.stream().mapToInt(Ball::runsScored).sum();
}
}
public class Ball {
private Player bowledBy;
private Player playedBy;
private BallType type;
private Wicket wicket; // null when no wicket fell
private final List<Run> runs = new ArrayList<>();
private Commentary commentary;
public BallType type() { return type; }
public List<Run> runs() { return Collections.unmodifiableList(runs); }
public boolean isWicket() { return wicket != null; }
public int runsScored() {
return runs.stream().mapToInt(Run::value).sum();
}
}Run, Wicket, and Commentary
public class Run {
private final int value; // number of runs this entry represents
private final RunType type; // how they were scored
public Run(int value, RunType type) {
this.value = value;
this.type = type;
}
public int value() { return value; }
public RunType type() { return type; }
}
public class Wicket {
private WicketType wicketType;
private Player playerOut;
private Player caughtBy; // for CAUGHT
private Player runOutBy; // for RUN_OUT
private Player stumpedBy; // for STUMPED
public WicketType wicketType() { return wicketType; }
public Player playerOut() { return playerOut; }
}
public class Commentary {
private String text;
private Date createdAt;
private Commentator createdBy;
}Innings and Match
With Over.balls() in place, the innings-level folds compile and read cleanly — each one streams the balls of every over.
public class Innings {
private int number;
private Date startTime;
private final List<Over> overs = new ArrayList<>();
public boolean addOver(Over over) { return overs.add(over); }
public int totalRuns() {
return overs.stream().mapToInt(Over::runs).sum();
}
// Folds over every ball in every over — relies on Over.balls().
public long wickets() {
return overs.stream()
.flatMap(o -> o.balls().stream())
.filter(Ball::isWicket)
.count();
}
}
public abstract class Match {
private int number;
private Date startTime;
private MatchResult result;
private MatchFormat format;
private Playing11[] teams;
private List<Innings> innings;
private List<Umpire> umpires;
private Referee referee;
private List<Commentator> commentators;
private List<MatchStat> matchStats;
public boolean assignStadium(Stadium stadium) { /* ... */ return true; }
public boolean assignReferee(Referee referee) { /* ... */ return true; }
}
public class ODI extends Match { /* ... */ }
public class T20 extends Match { /* ... */ }
public class Test extends Match { /* ... */ }Stat and StatQuery
Stat is the recorded fact; StatQuery is a reusable, named question evaluated across the match archive — that is how global queries like “highest wicket-taker of all time” are answered without coupling the question to any one match.
public class Stat {
private int matchesPlayed;
private int runsScored;
private int wicketsTaken;
private int centuries;
private int fifties;
private double battingAverage;
public int getWicketsTaken() { return wicketsTaken; }
public int getRunsScored() { return runsScored; }
public int getCenturies() { return centuries; }
}
public class PlayerStat extends Stat {
private Player player;
public String getPlayerName() { return player.getName(); }
}
public class MatchStat extends Stat { private Match match; }
public class TournamentStat extends Stat { private Tournament tournament; }
// A named, reusable question evaluated over the archive of matches.
// Concrete query: highest wicket-taker folds PlayerStat.wicketsTaken.
public class HighestWicketTakerQuery {
/**
* Folds every PlayerStat in the archive; returns the player id
* (or name) with the maximum wicketsTaken.
* Worked numbers: Bumrah 450, Anderson 700, Starc 400 → "Anderson".
*/
public String evaluate(List<PlayerStat> playerStats) {
if (playerStats == null || playerStats.isEmpty()) {
throw new IllegalArgumentException("empty archive");
}
PlayerStat best = playerStats.get(0);
for (PlayerStat ps : playerStats) {
if (ps.getWicketsTaken() > best.getWicketsTaken()) {
best = ps;
}
}
return best.getPlayerName(); // e.g. "Anderson" with 700
}
}
// Stream form of the same fold (equivalent result):
// playerStats.stream()
// .max(Comparator.comparingInt(PlayerStat::getWicketsTaken))
// .map(PlayerStat::getPlayerName)
// .orElseThrow();
// SQL-shaped view of the same question over a denormalized fact table:
// SELECT player_name, SUM(wickets) AS total
// FROM match_player_bowling
// GROUP BY player_id, player_name
// ORDER BY total DESC
// LIMIT 1;
// → Anderson | 700
Numeric walkthrough. Archive has three PlayerStat rows: Bumrah 450 wickets, Anderson 700, Starc 400. Fold initializes best = Bumrah, then upgrades to Anderson (700 > 450), then keeps Anderson (700 > 400). Result: "Anderson". A stub that return null cannot answer requirement 7; every global query is this same fold with a different projection (centuries, runs, average).
Scale reality: PlayerStat is a maintained aggregate, not a live fold over raw balls. The fold and the GROUP BY above are shown over a handful of PlayerStat rows for clarity — but "all time" spans every international match ever played, on the order of ~108 ball-level facts. You never recompute "highest wicket-taker of all time" by folding 108 balls on each request. Instead PlayerStat is a materialized aggregate: one row per player (a few thousand — ~103), kept current either incrementally as each ball is applied (the ball-apply path bumps the bowler's wicketsTaken, the batter's runsScored, etc.) or by a periodic batch recompute. The live query then folds ~103 maintained rows, never the ~108 raw facts. For hot top-k questions, keep the answer even cheaper with a sorted leaderboard (or an ORDER BY wickets DESC LIMIT k backed by an index on the aggregate column) so it is a bounded read rather than a full scan. This is the standard derived-data framing from Kleppmann's Designing Data-Intensive Applications (ch. 3): precompute and maintain the aggregate; do not fold the event log on every read.
Traced example: a wide that is misfielded
Suppose the innings total is 65 and the bowler has bowled 2 legal balls so far this over. The bowler sends down a wide; the keeper misfields it and the batters run once more before the ball is gathered.
A wide already includes its own 1-run penalty. The extra run from the misfield does not become a new category — it is attributed back to the same delivery as a further wide. So the ball is recorded as a WIDE delivery carrying two Run entries, both of type WIDE (the penalty plus the overthrow), totalling 2 runs added to the wides tally. (Byes and wides are mutually exclusive scoring categories, so none of this is a bye — there is no “bye-equivalent” here.)
Ball b = new Ball(); // type = WIDE
b.add(new Run(1, RunType.WIDE)); // the wide penalty
b.add(new Run(1, RunType.WIDE)); // extra run off the misfield — still a wideWhat changes, and what does not
| Quantity | Before | After | Why |
|---|---|---|---|
| Innings total | 65 | 67 | +2, both runs count toward the team total |
| Wickets fallen | 1 | 1 | No dismissal on this delivery |
| Legal balls this over | 2 | 2 | A wide is not a legal ball — it does not advance the over |
| Bowler balls bowled | n | n | Unchanged: the wide must be re-bowled |
| Bowler runs conceded | r | r + 2 | Both wides are charged to the bowler |
The arithmetic is the same as a naive “wide + overthrow” reading would give — but the attribution matters: on a page that promises all international cricket rules are followed, both runs are wides, recorded against the wides tally, not a separate overthrow run type.
Live ball-by-ball concurrency
Live commentary implies concurrent writers (scorer UI, official feed, retrying clients). Unordered inserts corrupt overs: ball 5 before ball 4, or two "ball 3" rows. The model must enforce a total order per over.
- Sequential ball numbers per over. Each ball carries
(matchId, innings, overNumber, ballSeq). Inserts must present the next expectedballSeq(or the next legal-ball index). Reject out-of-order with a conflict error; never silently reorder after the fact without an audit trail. - Single scorer session or CAS on over state. Prefer one authoritative scorer session per live match (optimistic lock / lease). If multiple writers are allowed, update
Over.legalBallCount(or last sequence) with compare-and-swap:UPDATE over SET legal_ball_count = ?, version = version + 1 WHERE id = ? AND version = ?. Loser retries or fails loud. - Commentary is not a second source of truth. Attach commentary to an already-accepted ball id; never let a comment create a ball row by itself.
// Sketch: claim next sequence under optimistic concurrency
boolean recordBall(long overId, int expectedSeq, Ball ball) {
// CAS: only succeed if over.next_seq == expectedSeq
int updated = db.update(
"UPDATE overs SET next_seq = next_seq + 1, version = version + 1 " +
"WHERE id = ? AND next_seq = ? AND version = ?",
overId, expectedSeq, ball.getOverVersion());
if (updated != 1) return false; // concurrent writer or out-of-order
db.insertBall(overId, expectedSeq, ball);
return true;
}
Without this, two scorer tabs can both insert "ball 3" and the innings total diverges from the ball list. Interviewers expect you to name the race and the CAS/lease fix, not only the domain classes.
Source
Adapted and corrected from Grokking the Object-Oriented Design Interview (Educative / Design Gurus), “Design Cricinfo,” with scoring attribution reconciled against the Laws of Cricket (MCC) governing wides, no-balls, byes, leg-byes, and overthrows. StatQuery evaluate and live-update concurrency elevated for this guide.
🤖 Don't fully get this? Learn it with Claude
Stuck on Design Cricinfo? Open Claude, copy a block below, and it'll teach you this exact concept — visually and interactively.
Build the mental picture, not memorization.
I just read a lesson on **Design Cricinfo** (OO & Low-Level Design) and want to truly understand it. Explain Design Cricinfo from first principles using ONE vivid real-world analogy and a visual mental model — draw it as ASCII art or a clear step-by-step diagram — with a concrete example using real numbers. Then ask me one question to check I got the mental picture, and wait for my reply. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
Socratic — adapts to where you're stuck.
Teach me **Design Cricinfo** interactively. Ask me ONE guiding question at a time, wait for my answer, and adapt to my confusion — build the idea with me step by step instead of explaining it all at once. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
Active recall exposes what you missed.
Quiz me on **Design Cricinfo** with 5 questions, easy to tricky, ONE at a time. Tell me if each answer is right; at the end, explain clearly what I got wrong and why. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
Intuition + hook + flashcards for long-term memory.
Help me remember **Design Cricinfo** for the long term: give the one-sentence intuition, a memorable hook/mnemonic, a tiny worked example, and 3 active-recall flashcards (Q -> A). If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.