DEV Community

Abhinav
Abhinav

Posted on • Edited on

๐Ÿ† How We Scaled Dense Ranking for a 7-Day Challenge Using MongoDB and Redis

Use case: "We had to build a live ranking system for a 7-day user challenge based on daily logs, with 10,000+ participants. Here's how we achieved accurate dense ranking efficiently."


๐Ÿง  The Problem

Our product team designed a 7-day streak challenge where users earn coins by logging their daily routines. We wanted to:

  • Show users their current rank
  • Show how many people theyโ€™re ahead of or behind
  • Update this every day, based on how many days theyโ€™ve logged
  • Support dense ranking (equal scores share the same rank)

What is Dense Ranking?

In dense ranking, if multiple users have the same score, they share the same rank, and the next rank increments by one โ€” not by the count.

Example:

Days Logged User ID Rank
5 U1 1
5 U2 1
4 U3 2
4 U4 2
3 U5 3

๐Ÿงฑ System Overview

๐Ÿ—„ MongoDB

  • We store logging data in a streak_logs collection:
{
  user_id: "abc123",
  first_date_of_log: "2025-07-06T00:00:00Z",
  last_date_of_log: "2025-07-10T00:00:00Z"
}
Enter fullscreen mode Exit fullscreen mode

โšก Redis

  • Every midnight, a cron job:

    • Computes the ranks
    • Caches results as:
    • BAH!CHALLENGE!RANK!MAP!YYYY-MM-DD
    • BAH!CHALLENGE!RANK!SUMMARY!YYYY-MM-DD

โš™๏ธ How We Computed Dense Ranks in JavaScript

Step 1: Fetch all eligible users in batches

async function* fetchEligibleUserIdsInBatches() {
  const cursor = UserTaskDetail.find({ is_active: true, task_id: CHALLENGE_TASK_ID }).select('user_id -_id').cursor();
  let batch = [];
  for await (const doc of cursor) {
    batch.push(doc.user_id);
    if (batch.length === 1000) {
      yield batch;
      batch = [];
    }
  }
  if (batch.length) yield batch;
}
Enter fullscreen mode Exit fullscreen mode

Step 2: Fetch each userโ€™s logged days

for (const log of allLogs) {
  const effectiveStart = moment.max(challengeStart, moment(log.first_date_of_log));
  const effectiveEnd = moment.min(challengeEnd, moment(log.last_date_of_log));
  const daysLogged = Math.max(effectiveEnd.diff(effectiveStart, 'days') + 1, 0);
  userDayMap[log.user_id] = daysLogged;
}
Enter fullscreen mode Exit fullscreen mode

Step 3: Group users by days logged

const scoreGroups = {};
for (const { userId, daysLogged } of usersWithDays) {
  if (!scoreGroups[daysLogged]) scoreGroups[daysLogged] = [];
  scoreGroups[daysLogged].push(userId);
}
Enter fullscreen mode Exit fullscreen mode

Step 4: Assign Dense Ranks

const sortedScores = Object.keys(scoreGroups).map(Number).sort((a, b) => b - a);
let rank = 1;
for (const score of sortedScores) {
  for (const userId of scoreGroups[score]) {
    userRankMap[userId] = rank;
  }
  rank += 1;
}
Enter fullscreen mode Exit fullscreen mode

Step 5: Cache the Results

const rankMapKey = `BAH!CHALLENGE!RANK!MAP!${istDateKey}`;
const rankSummaryKey = `BAH!CHALLENGE!RANK!SUMMARY!${istDateKey}`;

await Promise.all([
  cache.setAsync(rankMapKey, JSON.stringify(userRankMap), 'EX', 86400),
  cache.setAsync(rankSummaryKey, JSON.stringify(rankSummary), 'EX', 86400)
]);
Enter fullscreen mode Exit fullscreen mode

Step 6: Generate User Message on Frontend

const getRankStatus = (rankNumber, rankSummary) => {
  const totalParticipants = Object.values(rankSummary).reduce((sum, val) => sum + val, 0);
  if (rankNumber === 1) {
    const topUsers = rankSummary['1'] || 1;
    return `You're ahead of ${totalParticipants - topUsers} participants`;
  } else {
    return `You're behind ${rankNumber - 1} users`;
  }
};
Enter fullscreen mode Exit fullscreen mode

๐Ÿงช Results

  • Efficient computation of dense ranks for 10K+ users
  • Consistent Redis-based caching for real-time API usage
  • Easily extendable for longer challenges or other metrics

๐Ÿ’ก Learnings

  1. Batching Mongo queries avoids $in overloads.
  2. Dense ranking is perfect for fair user gamification.
  3. Precomputing + caching is the key to fast API response times.

๐Ÿ”š Final Thoughts

Dense ranking can be tricky at scale โ€” but with a bit of precomputation and smart caching, it becomes highly performant. If you're building habit-based features or gamified tasks, this pattern works beautifully.

Top comments (0)