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)