Skip to main content

ADR-0035 — match/detail_live is the only in-play source; match/live/history is a post-match archive

  • Status: Accepted
  • Date: 2026-08-27
  • Related: ADR-0034, docs/architecture/data-plane-integration.md §4

Context​

TheSports exposes two endpoints that return the same three-key shape — score, stats / timeline, players — and it is tempting to serve both a live scorecard and a finished one from whichever is convenient. Measured 2026-08-27, they are disjoint:

endpointfinished matchesin-play matches
cricket/match/live/history23 of 23 (100%)0 of 10 (0%)
cricket/match/detail_liveabsent by design7–8 in-play rows per snapshot

live/history is exactly what the provider's own product name says — "Statistical data (historical matches)". It is a post-match archive. Tennis behaves the same way (live/history 12/12 on finished, detail_live on live). Basketball's live/history returns {} for the three live matches probed.

Any design that serves both from one endpoint shows empty scorecards during play — the single worst failure mode for a live-score product, because it is invisible in testing against finished fixtures and total in production during a match.

The coverage.mlive flag does not predict live/history availability and may not be used to route between them.

A third property compounds this: match/detail_live has no "match ended" message. A match that finishes is simply absent from the next response. Absence is the signal.

Decision​

Phase decides the source, and the phase comes from the match record, not from a guess.

  1. In-play → match/detail_live (via the MQTT deltas merged into its snapshot). It is the only source of an in-play score, timeline or scorecard.
  2. Finished → match/live/history, on demand, by uuid.
  3. Scheduled → the diary record. No live call.
  4. The terminal status and the final score come from the diary, never from the live feed, because the live feed cannot say a match ended. The recent-window diary cycle exists solely to bound that lag to minutes.
  5. A live record carries a short TTL (4 × staleness, minimum 30s) so a match dropping out of the in-play feed stops being reported live on its own. The TTL is refreshed only by re-asking the provider — never by rewriting our own last known state, because a self-keepalive would keep a finished match resident too. That is the stale-live-record bug, reintroduced by our own cache.
  6. match/diary has a hard ±30-day window (bisected exactly; ±31d returns 405 Beyond the scope of account permissions) and defaults silently to today when called with no parameters. Anything older than 30 days must use match/season or match/list, which carry no such limit. A param-less code: 0 with empty results is not evidence that an endpoint is healthy.

Consequences​

  • Two code paths, deliberately, and the branch is on phase — which is derived from the provider's own status_id table, per sport, never shared between sports.
  • A cricket Test needs a 6-day diary lookback, not one day: the provider files a Test once at day one while the betting side re-dates the fixture as each day's play begins. Measured — a live Test with a full scorecard sat outside every window we pulled and rendered no score for days.
  • Any future "just cache the last live state" optimisation is forbidden by rule 5 and the reason must travel with the code, or it will be reintroduced.
  • Historical backfill beyond 30 days is a match/season job, not a diary job.