typescript 115 lines · 3 tabs

Redis-Backed Paginated Leaderboard With a Repository and Ranking Service

Shared by codesnips Aug 2026
3 tabs
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;
  }
}
3 files · typescript Explain with highlit

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

typescript
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

typescript reliability retry
by codesnips 2 tabs
ruby
module Api
  module V1
    class UsersController < BaseController
      def show
        user = User.includes(:profile).find(params[:id])

ETags for conditional requests and caching

rails caching http-caching
by Alex Kumar 1 tab
ruby
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

rails turbo hotwire
by codesnips 4 tabs
ruby
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

rails caching performance
by Alex Kumar 1 tab
typescript
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

react axios api
by Maya Patel 1 tab
typescript
import { create } from 'zustand'
import { persist } from 'zustand/middleware'

interface FilterState {
  search: string
  category: string | null

Zustand for lightweight state management

react zustand state-management
by Maya Patel 2 tabs

Share this code

Here's the card — post it anywhere.

Redis-Backed Paginated Leaderboard With a Repository and Ranking Service — share card
Link copied