import type { Redis } from "ioredis";
export interface RawEntry {
member: string;
score: number;
}
export class LeaderboardRepository {
constructor(private readonly redis: Redis, private readonly key: string) {}
async addScore(member: string, score: number): Promise<void> {
await this.redis.zadd(this.key, score, member);
}
async total(): Promise<number> {
return this.redis.zcard(this.key);
}
async rankOf(member: string): Promise<number | null> {
const rank = await this.redis.zrevrank(this.key, member);
return rank === null ? null : rank;
}
async pageByRank(start: number, stop: number): Promise<RawEntry[]> {
const flat = await this.redis.zrevrange(this.key, start, stop, "WITHSCORES");
const entries: RawEntry[] = [];
for (let i = 0; i < flat.length; i += 2) {
entries.push({ member: flat[i], score: Number(flat[i + 1]) });
}
return entries;
}
}
import { LeaderboardRepository, RawEntry } from "./LeaderboardRepository";
export interface LeaderboardEntry {
rank: number;
member: string;
score: number;
}
export interface LeaderboardPage {
page: number;
pageSize: number;
totalEntries: number;
totalPages: number;
entries: LeaderboardEntry[];
}
const MAX_PAGE_SIZE = 100;
export class RankingService {
constructor(private readonly repo: LeaderboardRepository) {}
private decorate(raw: RawEntry[], offset: number): LeaderboardEntry[] {
return raw.map((entry, index) => ({
rank: offset + index + 1,
member: entry.member,
score: entry.score,
}));
}
async getPage(page: number, pageSize: number): Promise<LeaderboardPage> {
const safeSize = Math.min(Math.max(1, Math.trunc(pageSize)), MAX_PAGE_SIZE);
const safePage = Math.max(1, Math.trunc(page));
const start = (safePage - 1) * safeSize;
const stop = start + safeSize - 1;
const [totalEntries, raw] = await Promise.all([
this.repo.total(),
this.repo.pageByRank(start, stop),
]);
return {
page: safePage,
pageSize: safeSize,
totalEntries,
totalPages: Math.max(1, Math.ceil(totalEntries / safeSize)),
entries: this.decorate(raw, start),
};
}
async getPlayerContext(member: string, radius = 3): Promise<LeaderboardEntry[] | null> {
const rank = await this.repo.rankOf(member);
if (rank === null) return null;
const start = Math.max(0, rank - radius);
const stop = rank + radius;
const raw = await this.repo.pageByRank(start, stop);
return this.decorate(raw, start);
}
}
import { Router, Request, Response } from "express";
import { RankingService } from "./RankingService";
export function leaderboardRoutes(service: RankingService): Router {
const router = Router();
router.get("/leaderboard", async (req: Request, res: Response) => {
const page = Number(req.query.page ?? 1);
const pageSize = Number(req.query.pageSize ?? 20);
const result = await service.getPage(page, pageSize);
res.json(result);
});
router.get("/leaderboard/around/:member", async (req: Request, res: Response) => {
const radius = req.query.radius ? Number(req.query.radius) : undefined;
const context = await service.getPlayerContext(req.params.member, radius);
if (context === null) {
return res.status(404).json({ error: "player is not ranked" });
}
res.json({ member: req.params.member, window: context });
});
return router;
}
A leaderboard is a classic case where a naive ORDER BY score LIMIT/OFFSET query against a relational table degrades badly under load: every page request re-sorts a large table and rank lookups for a single player scan the whole ordered set. This snippet models the feature around a Redis sorted set (ZSET), which keeps entries ordered by score and answers rank, range, and count queries in O(log N).
In LeaderboardRepository, the raw Redis access is isolated behind a small interface. addScore uses ZADD to upsert a member's score, so submitting a new high score simply replaces the old value in place. pageByRank calls ZREVRANGE with WITHSCORES to pull a contiguous slice in descending order, and rankOf uses ZREVRANK to find a single member's zero-based position without materialising the whole board. Keeping these primitives in a repository means the ranking logic never touches Redis command strings directly, which makes the service testable against an in-memory fake.
RankingService is where pagination and presentation live. getPage clamps the requested page and pageSize to safe bounds, converts the one-based page into the inclusive start/stop indices that ZREVRANGE expects, and computes a global rank for each row by adding the slice offset — Redis returns members in order, so the first row on page three is simply start + 1. The service also parses the flat [member, score, member, score] reply into typed LeaderboardEntry objects and attaches totalPages derived from ZCARD.
The getPlayerContext method shows the real payoff of storing rank in Redis: it finds a player's rank in O(log N) and then fetches a symmetric window around them, which is what most game UIs actually render ("you and your neighbours"). Reaching the top of the board is a rank of 0, and an unranked player returns null rather than throwing.
The trade-off is that Redis becomes a source of truth that must be kept consistent with the primary database, usually by writing scores through both or replaying from an event log; ties also resolve by lexical member order, which some products refine with a composite score. When read throughput on ranked data dominates, this pattern is the standard reach-for.
Related snips
export interface RetryOptions {
retries: number;
baseMs: number;
maxMs: number;
signal?: AbortSignal;
onRetry?: (attempt: number, delay: number, err: unknown) => void;
Exponential backoff with jitter for retries
module Api
module V1
class UsersController < BaseController
def show
user = User.includes(:profile).find(params[:id])
ETags for conditional requests and caching
class PostsController < ApplicationController
def index
@posts = Post.includes(:author)
.order(created_at: :desc)
.page(params[:page])
.per(10)
Turbo Frames: infinite scroll with lazy-loading frame
json.array! @posts do |post|
json.cache! ['v1', post], expires_in: 1.hour do
json.id post.id
json.title post.title
json.excerpt post.excerpt
json.published_at post.published_at
Fragment caching for expensive JSON serialization
import axios, { AxiosError } from 'axios'
import { v4 as uuidv4 } from 'uuid'
const api = axios.create({
baseURL: import.meta.env.VITE_API_URL || 'http://localhost:3000/api/v1',
timeout: 15000,
Axios API client with interceptors
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
interface FilterState {
search: string
category: string | null
Zustand for lightweight state management
Share this code
Here's the card — post it anywhere.