Real-Time Scores: Event Ordering Per Game Is Non‑Negotiable

Real-Time Scores: Event Ordering Per Game Is Non‑Negotiable

In any real-time sports scoring system, the single most critical — and often overlooked — requirement is strict event ordering per game. If a "goal at 60:00" is processed after a "red card at 61:00", the timeline, player stats, leaderboards, and fan notifications can all become inconsistent.
This isn't a UI problem or a flaky clock problem; it's an architectural requirement. Below is a concise, practical guide to designing systems that keep timelines correct and metrics reliable.
The problem, in one sentence
Out-of-order events lead to incorrect aggregates, double counts, wrong notifications, and mistrust. Timestamps alone are not enough — processing order matters.
Core principles
- Partition by game_id: Ensure all events for the same game are routed to the same ordered stream (topic partition or equivalent).
- Sequential processing per partition: Consume and apply events in the partition's order.
- Idempotency: Make handlers safe to run multiple times (use event_id as the dedupe key).
- Persist the event log first: Write the raw event to durable storage before updating derived state (score, PlayerStats, cache).
Recommended architecture
- Ingest events into a partitioned streaming system (e.g., Kafka) using game_id as the partition key. This guarantees per-game ordering within a partition.
- Have a consumer group where each consumer processes one or more partitions sequentially.
- For each event:
- Persist the raw event to an append-only event store (or commit the offset only after durable write).
- Deduplicate using event_id (a unique event identifier) to ensure idempotency.
- Apply the event to aggregates (score, player stats) and update caches/state stores.
- Emit derived events/notifications after state is updated.
This sequence (persist raw event → dedupe → update derived state → notify) preserves correctness across retries and failures.
Practical patterns and details
- Partitioning: Use game_id as the partition key. If a game has too much throughput, shard by game_id + shard_id but still maintain ordering within each shard.
- Exactly-once vs at-least-once: Exactly-once semantics help but are often complex. A simpler, reliable approach is at-least-once processing + idempotent handlers + durable event logging.
- Deduplication store: Keep a compact dedupe index (e.g., Redis, RocksDB, or a local state store) keyed by event_id with TTL if appropriate.
- Commit strategy: Only commit the stream offset after the event is durably persisted and applied. This prevents data loss and reduces risk of reprocessing without dedupe.
- Derived state persistence: Use transactional updates where possible (e.g., write-ahead log + atomic update to aggregates) to avoid partial updates.
- UI ordering: Drive the UI from the same event log or from the derived state that was built from the ordered stream. Do not rely solely on client-side sorting by timestamp.
Example pseudocode
for event in partitioned_stream(partition=game_id_partition):
if dedupe_store.contains(event.event_id):
continue
persist_raw_event(event) # durable append
dedupe_store.add(event.event_id)
apply_event_to_aggregates(event) # update score, player stats
update_cache_or_state_store()
emit_notifications(event)
commit_stream_offset(event)
Failure and recovery
- On consumer crash, the consumer restarts and resumes from the last committed offset. Because events were persisted and handlers are idempotent, reprocessing is safe.
- For long-running reprocesses (e.g., backfills), replay from the event log to rebuild derived state deterministically.
Interview-ready one-liner
"To guarantee correct real-time scores, partition the stream by game_id to preserve per-game order, make handlers idempotent using event_id, and always persist the raw event before updating derived state — ordering plus idempotency prevents double counts and out-of-order UIs."
Quick checklist
- [ ] Partition events by game_id
- [ ] Process each partition sequentially
- [ ] Persist raw event before state changes
- [ ] Use event_id for dedupe (idempotency)
- [ ] Commit offsets only after durable writes
- [ ] Drive UI from the event-derived state or event log
Keeping ordering and idempotency at the heart of your architecture makes real-time scoring robust, auditable, and easy to reason about — and it’s exactly what interviewers want to hear.


