Engineering Atlas › Low-Level Design

Code a live cricket scoreboard

13 min read · last reviewed 2026-10-17

Open in the interactive app: practice, quiz, mock interview →

In one minute

Requirements

Functional

Non-functional

Out of scope

Entities and relationships

Match has two Teams, a format (T20, ODI, Test) and a list of Innings. Innings is the aggregate this design is about. It owns the batting order, the over limit, and an append-only list of Event objects: OverStarted(bowler) or a Delivery (kind, runs, optional Dismissal, plus commentary text and a timestamp in production).

Scorecard is the projection, a plain data object rebuilt from the events. It holds the total, wickets, extras, legal balls bowled, the striker and non-striker, the next batter in, a BattingEntry per batter (runs, balls, how out), and a BowlingFigures per bowler (legal balls, runs conceded, wickets).

DeliveryRule is one strategy per delivery kind. It knows whether the ball is legal, whether the batter faced it, who gets the runs, and what is charged to the bowler. Dismissal knows whether the bowler is credited: bowled, caught and lbw are, run out is not.

ScoringService is the facade the scorer's app calls. It validates an event against the current scorecard, appends it, applies it, and publishes a BallRecorded event.

Class diagram

classDiagram
    class Innings {
        +battingOrder list
        +maxOvers int
        +events list~Event~
        +startOver(bowler)
        +ball(kind, runs, dismissal)
        +undo()
    }
    class Event {
        <<interface>>
        +validate(card)
        +apply(card)
    }
    class OverStarted {
        +bowler
    }
    class Delivery {
        +kind
        +runs int
        +dismissal
    }
    class DeliveryRule {
        <<interface>>
        +isLegal() bool
        +facedByBatter() bool
        +batterRuns(runs) int
        +extras(runs) int
        +bowlerCharge(runs) int
    }
    class WideRule
    class NoBallRule
    class ByeRule
    class RunRule
    class Scorecard {
        +total int
        +wickets int
        +legalBalls int
        +striker
        +nonStriker
        +batting map
        +bowling map
    }
    class ScoringService {
        +record(event)
        +undo()
        +subscribe(listener)
    }
    Event <|.. OverStarted
    Event <|.. Delivery
    Delivery --> DeliveryRule
    DeliveryRule <|.. WideRule
    DeliveryRule <|.. NoBallRule
    DeliveryRule <|.. ByeRule
    DeliveryRule <|.. RunRule
    Innings "1" *-- "*" Event
    Innings --> Scorecard
    ScoringService --> Innings

Key flows

The interesting flow is a correction. The umpire gives Ana out caught, the scorer records it, and a review then overturns it. The scorer presses undo. The service drops the last event and replays the rest into a fresh scorecard, so Ana's balls faced, the wicket count, the bowler's wickets and the strike all return to exactly where they were.

sequenceDiagram
    participant S as Scorer app
    participant SV as ScoringService
    participant I as Innings
    participant C as Scorecard
    participant L as Listeners
    S->>SV: ball(run, 0, caught)
    SV->>I: validate against card
    I->>C: apply: wickets 1, Cal on strike
    SV->>L: BallRecorded(3.4)
    S->>SV: undo()
    SV->>I: drop last event
    I->>C: rebuild by replaying 22 events
    C-->>I: wickets 0, Ana on strike
    SV->>L: BallCorrected(3.4)
    S->>SV: ball(run, 0)
    SV->>L: BallRecorded(3.4)

Listeners receive a correction as its own event. A commentary feed that already printed "OUT! Ana c Ben b Xi" appends a correction line instead of silently changing history, which is how live commentary is written by hand anyway.

Design patterns used and why

Event sourcing. The innings stores what happened, not the totals. Totals are a fold over events. This makes undo trivial and exact, gives a free audit trail for disputes, and lets you rebuild the scorecard after a rules bug is fixed. Replaying a full T20 innings is about 150 events, which takes microseconds.

Strategy for delivery kinds. WideRule, NoBallRule, ByeRule (with a leg-bye twin) and RunRule each answer the same five questions: legal? faced? runs to the batter? extras? charge to the bowler? Adding a free hit after a no-ball, or a penalty-run event, is a new rule rather than another branch in a 60-line apply method.

Command. Each scorer action is an object with validate and apply. Undo uses replay rather than an inverse per command, because inverses are easy to get wrong when a wicket also changed who is on strike.

State for the over. An over is not_started or in_progress; ball is valid only in progress, start_over only when not started, and six legal balls move it back.

Observer. Commentary, push alerts ("WICKET!"), the win predictor and the statistics database subscribe to BallRecorded and BallCorrected. The scoring core never waits on them.

Concurrency and edge cases

One writer per innings

Scoring is a single-writer problem: one official scorer per match, sometimes with a backup. Give each innings a sequence number and make the client send expected_seq with every event, like a compare-and-set. A second device, or a retry of a request that actually succeeded, gets a conflict instead of recording the same ball twice. In memory, a mutex around validate, append and apply is enough.

Retries and duplicates

A scorer on stadium Wi-Fi taps "4" and the request times out. The retry carries the same (innings, seq); the server sees an identical event already stored there and returns success without appending, the same idempotency-key idea used for payments.

The rules that trip people up

A wide adds one run plus anything the batters run, all as extras. It is not a legal ball and the batter has not faced it. A no-ball adds one extra, and any runs off the bat go to the batter. It is not legal, but it counts as a ball faced. Byes and leg byes are legal and faced, but go to extras and are not charged to the bowler. Since the 1980s, wides and no-balls have been charged to the bowler. A run out is not credited to the bowler. Under the 2022 laws, a new batter comes in on strike after a catch whether or not the batters crossed, which matches "replace the dismissed striker".

Ends of innings and older corrections

The innings ends when the side is all out (wickets equal batters minus one) or the final over's last legal ball is bowled; both checks run after every delivery. To fix a ball three overs ago, apply "replace event at sequence n" and replay from there with the same projection code.

Down the stack

Layer 1The contract

The scorecard equals fold(apply, empty_scorecard, events) for the stored events, in order. Each event was valid against the scorecard produced by the events before it. undo yields exactly the scorecard of the shorter log. Overs shown as O.B count only legal balls. A batter's balls include no-balls but not wides. Bowler runs include wides and no-balls but not byes or leg byes. A rejected event changes nothing and is not stored.

Layer 2The mechanism

Each delivery is applied in O(1): look up the rule, update a handful of integers in two maps, maybe swap the strike, maybe advance the next-batter index. Undo is O(n) in the innings' events: about 150 for a T20, a few hundred for an ODI, and over 1,000 for a long Test innings. If that matters, snapshot every over and replay from the latest snapshot, which is how event-sourced systems bound rebuild time. In a database the log is a table with primary key (innings_id, seq), enforcing one event per position, and the scorecard is a cached row.

Layer 3The machine

A delivery event is tiny: kind, runs and dismissal fit in a byte or two, and with ids and a timestamp it is about 32 bytes. A whole ODI innings is around 20 KB, which fits in the L1 data cache of any modern CPU, so a full replay is a few microseconds of integer arithmetic. The expensive part is everything after the write. An update packet with the new score and a commentary line is roughly 200 to 500 bytes. At 10 million connected readers during a big match, one ball means 2 to 5 GB of outbound data, delivered within seconds and repeated about every 30 to 40 seconds. That is why such sites push deltas through CDNs and fan-out tiers rather than serving each reader from the scoring database.

Why it's built this way

Scorers make mistakes and reviews overturn decisions, so the system must undo exactly, so it stores events and derives totals. Cricket's rules give each delivery kind different effects on five counters, so each kind is a strategy and the projection code stays readable. An innings has a single official scorer, so a sequence number and a mutex give total order without distributed consensus. Reads outnumber writes by millions to one, so the write path is a small in-memory projection and the read path is a fan-out of small deltas.

Extensibility

Leadership lens

Check yourself

  1. Over 7 starts. Xi bowls a wide that the batters run for one, then a no-ball hit for four. What are the overs, the extras added, the batter's balls faced, and the runs charged to Xi?

    Show answer

    Overs stay at 6.0, because neither ball is legal. Extras rise by 3: 2 from the wide and 1 from the no-ball. The batter who hit the four gets 4 runs and 1 ball faced, from the no-ball only. Xi is charged 7 runs.

  2. Why rebuild the scorecard by replay on undo instead of applying an inverse of the last delivery?

    Show answer

    A delivery can change many things at once: a wicket, who is on strike, the end of an over, the end of the innings. Writing a correct inverse for every combination is error prone. Replaying a short log is cheap and correct by construction.

  3. The batters take three byes off the fifth ball of an over. Who faces the sixth ball, and who faces the first ball of the next over?

    Show answer

    Three is odd, so the non-striker is now on strike for ball six. If ball six is a dot, the end-of-over swap puts the batter who faced ball five back on strike for the first ball of the next over, bowled from the other end.

  4. Two scorer devices both submit ball 4.3, one of them a retry. How does the system make sure the ball is recorded once?

    Show answer

    Each event carries the expected sequence number. The first one to arrive is appended. An identical retry is acknowledged without appending, and a different event at the same position is rejected as a conflict.

Watch

Free resources

Open in the interactive app: practice, quiz, mock interview →