Skip to content
Menu
Portfolio
  • Artificial Intelligence
  • Personal Projects
  • Assignments
  • Algorithms
  • Notes
  • Home
Portfolio

TechPi Forum

Posted on July 12, 2025July 29, 2025

User activity ranking system

In TechPi, a user activity leaderboard is provided. While a blog community would usually rank authors, we chose to highlight user activity to encourage greater participation. We provide daily and monthly leaderboard variants.

User activity score calculation rules:

  • Visiting a new page: +1 point
  • Liking or bookmarking an article: +2 points
    Canceling a like/bookmark: −2 points
  • Commenting on an article: +3 points
    Delete comment on an articel: -3 points
  • Publishing an approved article: +10 points

Design

The leaderboard business logic is relatively straightforward, making data structure design simple as well.

Data model for a leaderboard entry:

long userId; // user identifier
long rank;   // user's rank in the leaderboard
long score;  // user's accumulated activity score

Initial data structure consideration:
A LinkedList was considered since rankings are continuous and changes in position don’t require costly array copying. However, it has several downsides:

Problems with LinkedList

  1. Retrieving a user’s rank is inefficient (O(n))—random access is slow.
  2. Concurrency issues arise when multiple users update scores simultaneously.

Rather than building a custom structure from scratch, Redis provides an elegant and efficient solution using its ZSet (sorted set).

Redis-Based Approach

Redis’s ZSet is perfect for this use case:

  • Ensures uniqueness of elements (users)
  • Each element (user) has a score (activity)
  • Maintains elements sorted by score

By using ZSet, we store user scores directly, and Redis handles the ranking automatically.


Leaderboard Implementation

1. Updating User Activity Scores

Business logic steps:

  1. Compute the score change based on the activity type.
  2. For score increases:
    • Check for idempotency to avoid double-counting..
      • Store the user’s activity history directly in a Redis hash structure, with one record per day.
      • Key: activity_rank_{user_id}_{yyyyMMdd}
      • Field: unique key representing the type of activity (e.g., “article_123_praise”)
      • Value: the score added for that activity
    • If already added, return early.
    • Otherwise, proceed and record the operation.
  3. For score decreases:
    • Only deduct if a prior addition exists.
    • Prevent negative scores.

Here’s the core logic for updating activity scores:

public void modifyActivityScore(Long userId, ActivityScoreEntity activityScore) {
    if (userId == null) return;

    // Determine activity type and score
    String field;
    int score = 0;
    // visiting new page
    if (activityScore.getPath() != null) {
        field = "path_" + activityScore.getPath();
        score = 1;
    } else if (activityScore.getArticleId() != null) {
        field = activityScore.getArticleId() + "_";
        // (un)like an article
        if (activityScore.getPraise() != null) {
            field += "praise";
            score = BooleanUtils.isTrue(activityScore.getPraise()) ? 2 : -2;
        // (un)bookmark an article
        } else if (activityScore.getCollect() != null) {
            field += "collect";
            score = BooleanUtils.isTrue(activityScore.getCollect()) ? 2 : -2;
        // (un)Commenting on an article
        } else if (activityScore.getRate() != null) {
            field += "rate";
            score = BooleanUtils.isTrue(activityScore.getRate()) ? 3 : -3;
        // publish an article
        } else if (BooleanUtils.isTrue(activityScore.getPublishArticle())) {
            field += "publish";
            score += 10;
        }
    // (un)follow a user
    } else if (activityScore.getFollowedUserId() != null) {
        field = activityScore.getFollowedUserId() + "_follow";
        score = BooleanUtils.isTrue(activityScore.getFollow()) ? 2 : -2;
    } else {
        return;
    }

    final String todayRankKey = todayRankKey();
    final String monthRankKey = monthRankKey();
    final String userActionKey = ACTIVITY_SCORE_KEY + userId + DateUtil.format(DateTimeFormatter.ofPattern("yyyyMMdd"), System.currentTimeMillis());

    Integer existingScore = RedisClient.hGet(userActionKey, field, Integer.class);
    if (existingScore == null) {
        // No prior score -> add new entry
        if (score > 0) {
            RedisClient.hSet(userActionKey, field, score);
            RedisClient.expire(userActionKey, 31 * DateUtil.ONE_DAY_SECONDS);
            RedisClient.zIncrBy(todayRankKey, String.valueOf(userId), score);
            RedisClient.zIncrBy(monthRankKey, String.valueOf(userId), score);
        }
    } else if (existingScore > 0 && score < 0) {
        // Prior score exists -> allow deduction
        if (RedisClient.hDel(userActionKey, field)) {
            RedisClient.zIncrBy(todayRankKey, String.valueOf(userId), score);
            RedisClient.zIncrBy(monthRankKey, String.valueOf(userId), score);
        }
    }
}

2. Querying the Leaderboard

Now that we’re tracking scores in Redis, querying the leaderboard is simple.

  1. Use zRevRangeWithScores to get the top N users.
  2. Fetch user details from DB/cache.
  3. Format and return leaderboard entries with rank and score.
@Override
public List<RankItemDTO> queryRankList(ActivityRankTimeEnum time, int size) {
    // 1. Determine Redis key based on the requested time period (daily or monthly)
    String rankKey = time == ActivityRankTimeEnum.DAY ? getTodayRankKey() : getMonthRankKey();

    // 2. Get top N active users and their scores from Redis
    List<ImmutablePair<String, Double>> rankList = RedisClient.zTopNScore(rankKey, size);
    if (CollectionUtils.isEmpty(rankList)) {
        return Collections.emptyList();
    }

    // 3. Map userId (String) to score
    Map<Long, Integer> userScoreMap = rankList.stream()
            .collect(Collectors.toMap(
                pair -> Long.valueOf(pair.getLeft()),
                pair -> pair.getRight().intValue()
            ));

    // 4. Batch query user basic information
    List<SimpleUserInfoDTO> users = userService.batchQuerySimpleUserInfo(userScoreMap.keySet());

    // 5. Build final ranked list (Redis already sorted the results)
    List<RankItemDTO> rank = new ArrayList<>();
    for (SimpleUserInfoDTO user : users) {
        Integer score = userScoreMap.getOrDefault(user.getUserId(), 0);
        rank.add(new RankItemDTO()
                .setUser(user)
                .setScore(score));
    }

    // 6. Assign rank numbers (1-based)
    IntStream.range(0, rank.size())
            .forEach(i -> rank.get(i).setRank(i + 1));

    return rank;
}
public static List<ImmutablePair<String, Double>> zTopNScore(String key, int n) {
    return template.execute((RedisCallback<List<ImmutablePair<String, Double>>>) connection -> {
        // Use zRevRangeWithScores to get top N elements in descending score order
        Set<RedisZSetCommands.Tuple> set = connection.zRevRangeWithScores(keyBytes(key), 0, n - 1);
        if (set == null) {
            return Collections.emptyList();
        }

        // Convert Redis Tuple into (userId, score) pairs
        return set.stream()
                .map(tuple -> ImmutablePair.of(
                    toObj(tuple.getValue(), String.class),
                    tuple.getScore()))
                .collect(Collectors.toList());
    });
}
Pages: 1 2 3 4 5

CATEGORIES

  • Artificial Intelligence
  • Personal Projects
  • Notes
  • Algorithms

  • University of Maryland
  • CMSC426 - Computer Vision
  • CMSC320 - Introduction to Data Science
  • CMSC330 - Organization of Programming Languages
  • CMSC216 - Introduction to Computer Systems
©2025 Portfolio | WordPress Theme by Superbthemes.com