14 KiB
2.4 Leaderboard Service (Port 8083) - Detailed Implementation Plan
Summary
Implement the Leaderboard Service as the read-optimized ranking and statistics service for game outcomes, consistent with implementation decisions used in 2.1 (Question Bank), 2.2 (User), and 2.3 (Game Session).
Runtime stack and conventions:
- Fiber HTTP service with shared bootstrap and observability.
- PostgreSQL persistence with
EnsureSchema(ctx)startup DDL. - Redis for read-optimized ranking cache and top-10 acceleration.
- Shared
backend/sharedpackages for auth, errors, validation, logging, tracing, metrics, and readiness.
Scope boundary:
- Modify only
backend/services/leaderboard-service/**. - Do not modify
backend/services/*other than leaderboard-service. - Do not modify
backend/shared/**.
Decisions Reused from 2.1, 2.2, and 2.3
- Service composition pattern:
internal/infra/config.FromEnv()- logger/metrics/tracer initialization in
cmd/main.go - repository initialization +
EnsureSchema(ctx)at startup /health,/ready,/metricsregistration- route registration via
internal/interfaces/http/routes.go
- Persistence and state pattern:
- PostgreSQL as source of truth.
- Redis as optional performance layer, non-fatal when unavailable.
- service remains functional on PostgreSQL when Redis is down.
- Error and transport pattern:
- domain/application errors mapped via shared
httputil.SendError. - standard response envelope style (
success,data) used by existing services.
- Inter-service integration approach:
- HTTP adapters with application interfaces so transport can evolve later without domain changes.
- explicit DTOs for upstream/downstream contracts.
- Test pyramid:
- unit tests for ranking/statistics logic.
- HTTP integration tests with in-memory doubles/fakes.
- optional DB-backed integration tests gated by environment.
Objectives
- Provide public leaderboard query endpoints:
- top 10 scores
- player ranking and history
- global statistics
- Provide internal score ingestion endpoint for completed sessions.
- Ensure deterministic ranking:
- sort by score descending, then completion duration ascending, then completed_at ascending.
- Maintain historical score records for analytics and auditability.
- Deliver production-ready observability, readiness checks, and test coverage.
API Endpoints
GET /leaderboard/top10GET /leaderboard/players/:idGET /leaderboard/statsPOST /leaderboard/update(internal command endpoint)
Auth and Access Rules
- Query endpoints:
GET /leaderboard/top10,GET /leaderboard/statsare public read endpoints.GET /leaderboard/players/:idrequires auth; user can read own detailed history.- Admin role may read any player history.
- Update endpoint:
POST /leaderboard/updateis internal-only and requires service/admin auth middleware.- Reject anonymous or non-privileged callers.
Inter-Service Contracts
Game Session dependency
Purpose: ingest canonical final session outcomes.
Contract for POST /leaderboard/update request:
session_id(string, required)player_id(string, required)player_name(string, required)total_score(int, required, >= 0)questions_asked(int, required, >= 0)questions_correct(int, required, >= 0)hints_used(int, required, >= 0)duration_seconds(int, required, >= 0)completed_at(RFC3339 timestamp, required)completion_type(completed|timed_out|abandoned, required)
Idempotency decision:
- deduplicate on
session_id(unique). - repeated update for same
session_idreturns success with existing persisted record.
User Service dependency
Purpose: optional profile hydration fallback for player display fields.
Decision:
- leaderboard update request is authoritative for
player_nameto avoid hard runtime coupling. - optional future enrichment can call
GET /users/:id, but is not required for step2.4.
Domain Model
Aggregates:
LeaderboardEntry(one per completed session)PlayerRankingSnapshot(derived read model)
Value objects:
Rank(positive integer)SuccessRate(0..100 percentage)CompletionType
Domain services:
RankingService(ordering and tie-breaks)StatisticsService(global aggregates)
Core invariants:
- Each
session_idcan be ingested once. - Score cannot be negative.
- Questions counts cannot be negative.
questions_correct <= questions_asked.- Rank ordering rule is deterministic:
- score desc
- duration asc
- completed_at asc
- Top10 response always returns at most 10 entries.
Data Model (PostgreSQL)
leaderboard_entries
id UUID PRIMARY KEYsession_id VARCHAR(64) NOT NULL UNIQUEplayer_id VARCHAR(128) NOT NULLplayer_name VARCHAR(50) NOT NULLscore INT NOT NULLquestions_asked INT NOT NULLquestions_correct INT NOT NULLhints_used INT NOT NULL DEFAULT 0duration_seconds INT NOT NULLsuccess_rate NUMERIC(5,2) NOT NULLcompletion_type VARCHAR(16) NOT NULLcompleted_at TIMESTAMPTZ NOT NULLcreated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
Indexes:
(score DESC, duration_seconds ASC, completed_at ASC)(player_id, completed_at DESC)(completion_type, completed_at DESC)(created_at DESC)
leaderboard_player_stats
Pre-aggregated player read model for fast rank/profile reads.
player_id VARCHAR(128) PRIMARY KEYplayer_name VARCHAR(50) NOT NULLgames_played INT NOT NULL DEFAULT 0games_completed INT NOT NULL DEFAULT 0total_score BIGINT NOT NULL DEFAULT 0best_score INT NOT NULL DEFAULT 0avg_score NUMERIC(10,2) NOT NULL DEFAULT 0avg_success_rate NUMERIC(5,2) NOT NULL DEFAULT 0total_questions BIGINT NOT NULL DEFAULT 0total_correct BIGINT NOT NULL DEFAULT 0best_duration_seconds INT NULLlast_played_at TIMESTAMPTZ NULLupdated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
Indexes:
(best_score DESC, best_duration_seconds ASC, last_played_at ASC)(last_played_at DESC)
Note:
- all write updates to
leaderboard_player_statshappen transactionally withleaderboard_entriesinsert in the same repository method.
Redis Usage
Key prefix: lb:
Keys:
lb:top10:v1-> serialized top10 payload (TTL cache)lb:rank:{player_id}-> cached player rank snapshotlb:stats:global:v1-> cached global stats payloadlb:zset:scores-> sorted set score index (optional acceleration, non-authoritative)
Rules:
- PostgreSQL remains source of truth.
- On successful update ingestion:
- invalidate
lb:top10:v1 - invalidate
lb:stats:global:v1 - invalidate
lb:rank:{player_id} - best-effort Redis operations; failures logged and counted, not fatal.
- If Redis is unavailable:
- query endpoints compute from PostgreSQL and still return success.
- readiness check marks Redis as down but optional.
Endpoint Behavior (Decision Complete)
POST /leaderboard/update
Input: final session outcome payload (see contract above).
Flow:
- authenticate internal caller.
- validate input fields and invariants.
- start transaction.
- check existing by
session_id. - if existing:
- return existing normalized entry response (
200, idempotent success).
- insert into
leaderboard_entries. - upsert/refresh
leaderboard_player_statsaggregate. - commit transaction.
- invalidate related Redis caches.
- emit structured log + metrics counter.
Output:
- persisted leaderboard entry summary including computed
success_rate.
GET /leaderboard/top10
Query params:
- optional
completion_typefilter (completed|timed_out|abandoned) - optional
window(24h|7d|30d|all, defaultall)
Flow:
- attempt Redis cache hit for matching key variant.
- on miss, query PostgreSQL ordered by ranking rule.
- compute rank values (1..N), cap at 10.
- cache result with short TTL.
Output:
- ordered top list with rank, player_id, player_name, score, questions_asked, success_rate, duration_seconds, completed_at.
GET /leaderboard/players/:id
Auth:
- self or admin.
Query params:
page(default 1)page_size(default 20, max 100)
Flow:
- auth and ownership/admin check.
- fetch player aggregate from
leaderboard_player_stats. - fetch paginated history from
leaderboard_entries. - compute current global rank from ordering criteria against all players using
best_scorethen tie-breakers.
Output:
- player summary:
- current_rank, games_played, best_score, avg_score, avg_success_rate, total_score
- paginated history list.
GET /leaderboard/stats
Flow:
- attempt Redis cache hit.
- on miss, aggregate from PostgreSQL.
Returned stats:
total_gamestotal_playersavg_scoreavg_success_ratemax_scorescore_p50,score_p90,score_p99updated_at
Package Layout
backend/services/leaderboard-service/cmd/main.gobackend/services/leaderboard-service/internal/infra/config/config.gobackend/services/leaderboard-service/internal/domain/leaderboard/backend/services/leaderboard-service/internal/application/leaderboard/backend/services/leaderboard-service/internal/infra/persistence/ent/backend/services/leaderboard-service/internal/infra/state/backend/services/leaderboard-service/internal/interfaces/http/backend/services/leaderboard-service/tests/
Configuration
Service-specific:
LEADERBOARD_PORT(default8083)LEADERBOARD_TOP_LIMIT(default10)LEADERBOARD_PLAYER_HISTORY_DEFAULT_LIMIT(default20)LEADERBOARD_PLAYER_HISTORY_MAX_LIMIT(default100)LEADERBOARD_CACHE_TTL(default60s)LEADERBOARD_UPDATE_REQUIRE_AUTH(defaulttrue)
Optional integration:
GAME_SESSION_BASE_URL(defaulthttp://localhost:8080) for future backfill toolingUPSTREAM_HTTP_TIMEOUT(default3s)
Shared:
POSTGRES_*,REDIS_*,TRACING_*,METRICS_*,LOG_LEVEL,ZITADEL_*
Implementation Work Breakdown
Workstream A - Bootstrap, config, wiring
- Add
internal/infra/config/config.goenv parsing. - Wire logger/metrics/tracer in
cmd/main.go. - Initialize postgres + redis clients.
- Initialize repository and
EnsureSchema(ctx). - Register
/health,/ready,/metrics. - Build auth middleware and register routes.
Workstream B - Domain and application
- Define domain entities, value objects, and domain errors.
- Implement ranking and statistics calculation services.
- Implement application use-cases:
UpdateScoreGetTop10GetPlayerRankingGetGlobalStats
Workstream C - Persistence
- Implement repository interfaces and SQL-backed repository.
- Add
EnsureSchema(ctx)DDL for both tables and indexes. - Implement transactional ingestion:
- insert entry
- upsert player stats
- Implement top10/history/stats query methods.
Workstream D - HTTP interface
- Add request/response DTOs with validation tags.
- Implement handlers with shared error mapping.
- Apply ownership/admin checks for player history endpoint.
- Keep response envelope consistent with existing services.
Workstream E - Cache and read optimization
- Add Redis cache adapter for top10/stats/rank snapshots.
- Implement cache keys, invalidation, and graceful fallback.
- Add counters for cache hit/miss and invalidation failures.
Workstream F - Testing
- Unit tests:
- ranking tie-break correctness
- success-rate calculation
- stats aggregate math
- update idempotency behavior
- HTTP integration tests:
- update + top10 happy path
- duplicate update idempotency
- player endpoint auth guards
- stats endpoint and metrics availability
- Optional DB-backed tests (env-gated):
- schema creation
- transactional consistency
- unique
session_idconstraint
Error Handling Contract
400: invalid input or invariant violation401: missing/invalid auth for protected endpoints403: ownership/admin violation404: player not found (for player ranking endpoint)409: conflicting update (non-idempotent invalid duplicate payload scenario)500: unexpected internal failures
Observability
- Structured logs:
- include
session_id,player_id, endpoint, and operation outcome. - avoid PII-heavy payload logging.
- Metrics:
leaderboard_updates_total{status}leaderboard_top10_requests_total{cache}leaderboard_player_requests_total{status}leaderboard_stats_requests_total{cache}leaderboard_update_latency_seconds
- Tracing:
- endpoint -> application -> repository/cache spans.
- include attributes for cache hit/miss and DB query class.
Delivery Sequence (3-4 Days)
- Day 1: bootstrap/config + schema + repository scaffolding.
- Day 2:
POST /leaderboard/updatetransactional ingestion + cache invalidation. - Day 3: query endpoints (
top10,players/:id,stats) + auth checks. - Day 4: tests, observability hardening, bugfix buffer.
Verification Commands
From backend/services/leaderboard-service:
go test ./...
go vet ./...
Optional workspace-level check from backend:
go test ./...
Definition of Done
- All four endpoints implemented and route protection rules enforced.
- Ranking order and tie-break behavior matches functional requirement.
- Update ingestion is idempotent by
session_id. /health,/ready,/metricsfunctional with meaningful checks.- Redis cache fallback behavior works when Redis is unavailable.
go test ./...andgo vet ./...pass for leaderboard-service.- No code changes outside
backend/services/leaderboard-service/**.
Assumptions and Defaults
- Inter-service transport remains HTTP in phase 2.
- Leaderboard consistency can be eventual within seconds.
POST /leaderboard/updateis called by internal trusted workflow after session termination.- No changes are made in other services or
backend/sharedduring this step.