This document describes the technical design for a real-time vocabulary quiz feature that supports:
- Joining a quiz by unique
quiz_id - Real-time answer submission
- Server-authoritative score updates
- Live leaderboard updates
Targets (from PRD):
- Concurrent users per quiz session:
>= 100 - Score update latency:
< 300ms(server receive -> server emit) - Leaderboard update latency:
< 500ms(server receive -> server emit) - Availability during active quizzes:
99.9% - Score consistency:
100%(server-authoritative scoring)
flowchart LR
subgraph clientApps [ClientApps]
WebClient[WebClient_or_TestClient]
end
subgraph realtimeLayer [RealTimeCommunicationLayer]
WsApi[FastAPI_WebSocket_API]
end
subgraph backend [Backend]
QuizEngine[QuizEngine_Service]
SessionStore[SessionStateStore]
QuestionBank[QuestionBank_Predefined]
end
subgraph dataLayer [DataLayer]
Redis[(Redis_optional)]
Postgres[(Postgres_optional)]
end
WebClient <-->|WebSocket_events| WsApi
WsApi --> QuizEngine
QuizEngine --> QuestionBank
QuizEngine --> SessionStore
SessionStore --> Redis
SessionStore --> Postgres
- Connects via WebSocket, joins a quiz via
quiz_id - Submits answers during an active quiz
- Renders live leaderboard updates without page refresh
- On reconnect, re-joins and accepts a server snapshot to resync
- Manages WebSocket connections and maps sockets to a
quiz_idroom - Validates incoming event payloads and routes to
QuizEngine - Broadcasts server events (score updates, leaderboard updates, resync snapshots)
- Handles disconnects and implements heartbeats (ping/pong)
- Implementation note: the WebSocket API layer is organized into:
- DTO schema (
ws_models.py), registry-based dispatcher (ws_dispatcher.py), typed handlers (ws_handlers.py), and a thin transport loop (ws.py)
- DTO schema (
- Single source of truth for scoring and leaderboard ordering
- Enforces deterministic scoring rules and tie-breakers
- Enforces idempotency (prevents double scoring on retries/reconnects)
- Produces versioned state updates (monotonic session version)
- Provides predefined quiz questions available at session start (per PRD)
- Challenge scope: static JSON / in-memory fixtures
A storage abstraction for quiz state operations:
- Get or create session by
quiz_id - Upsert participant
- Update score
- Read leaderboard snapshot /
topN - Provide snapshot for join/reconnect (resync)
Implementations:
InMemorySessionStateStore(MVP, single-node)RedisSessionStateStore(optional scale-ready mode)
- Shared state for horizontally scaled deployments
- Efficient leaderboard operations (Sorted Sets) and fast increments
- Optional pub/sub to propagate updates across instances
- Not required by PRD (persistent profiles/sessions out of scope)
- Future: audits, analytics, replay, enterprise reporting
quiz_id: strstatus: lobby | active | endedversion: int(monotonic)participants: map[user_id -> Participant]leaderboard: list[LeaderboardEntry](cached topN)
user_id: strdisplay_name: strscore: intlast_client_seq: int(idempotency guard)
user_id: strdisplay_name: strscore: intrank: int
quiz_id: struser_id: strquestion_id: stranswer: strclient_seq: int
join_quiz:{ quiz_id, user_id, display_name }submit_answer:{ question_id, answer, client_seq }ping:{ ts }(optional)
joined:{ quiz_id, version, participant_score, leaderboard }score_updated:{ quiz_id, version, user_id, score, correct }leaderboard_updated:{ quiz_id, version, leaderboard }state_resync:{ quiz_id, version, participant_score, leaderboard, reason }error:{ code, message }
sequenceDiagram
participant C as Client
participant WS as FastAPI_WS_API
participant QE as QuizEngine
participant SS as SessionStateStore
C->>WS: connect(WebSocket)
C->>WS: join_quiz{quiz_id,user_id,display_name}
WS->>QE: join_quiz(...)
QE->>SS: upsert_participant(quiz_id,user_id,display_name)
QE->>SS: get_snapshot(quiz_id,user_id)
QE-->>WS: joined{version,participant_score,leaderboard}
WS-->>C: joined{version,participant_score,leaderboard}
C->>WS: submit_answer{question_id,answer,client_seq}
WS->>QE: submit_answer(...)
QE->>QE: validate_payload_and_question()
QE->>QE: idempotency_check(user_id,client_seq)
QE->>QE: score_answer_deterministically()
QE->>SS: update_score_and_version(quiz_id,user_id,delta)
QE->>SS: get_leaderboard_topN(quiz_id)
QE-->>WS: score_updated{version,user_id,score}
QE-->>WS: leaderboard_updated{version,leaderboard}
WS-->>C: score_updated{...}
WS-->>C: leaderboard_updated{...}
- Clients display scores computed by server only.
- Ensures “100% score consistency” across clients.
- Answer evaluation uses deterministic normalization (e.g., trim + casefold).
- Leaderboard tie-break is deterministic (score desc, then
user_idlexicographic ascending).
- Each user includes
client_seqthat increments per submission. - Server stores
last_client_seqand ignores submissions whereclient_seq <= last_client_seq.
- Session has monotonic
versionincremented on each accepted scoring mutation. - Clients apply only events with
versiongreater than their current version; otherwise they request/receivestate_resync.
- For
>= 100users/session, recomputingtopNby sorting in-memory on each update is acceptable for MVP. - For larger scale, use Redis Sorted Sets:
ZINCRBYfor score incrementsZREVRANGEfortopNretrieval
- Broadcast only
topN+ the user’s score (not full participant list) to reduce payload size. - Coalesce bursts (if multiple updates in quick succession, broadcast the latest state version).
- Heartbeats detect dead sockets quickly and cleanly.
- Reconnect path: client reconnects ->
join_quiz-> server returnsjoined/state_resyncsnapshot. - Graceful errors:
- invalid
quiz_id - unknown
question_id - malformed payloads
- duplicate/out-of-order
client_seq
- invalid
- Minimal web client or test harness
- Why: challenge focuses on the real-time engine; a thin client validates correctness and latency
- WebSocket
- Why: push-based low-latency updates without polling
- Python 3.x + FastAPI
- Why: rapid iteration, async-friendly, strong request validation (Pydantic), straightforward WebSocket support
- Uvicorn
- Why: stable ASGI server with good WebSocket performance
- Redis
- Why: shared state for horizontal scaling and efficient leaderboard operations
- Postgres
- Why: audit/history/analytics if requirements expand (not required for challenge)
- pytest + pytest-asyncio
- Why: validate scoring determinism, idempotency, reconnect/resync behaviors
- Locust (Python) or k6 (JS)
- Why: simulate
>= 100concurrent users and measure p95 latency against PRD targets
- Structured logging (stdlib logging or structlog)
- Implemented: standard library
loggingwith JSON output (seeserver/src/quiz_server/observability/logging.py)
- Implemented: standard library
- Metrics (optional): Prometheus-compatible counters/histograms
- Why: measurable latency and operational confidence; aligns with “monitoring and observability” requirement