Backend Development 20 min read

Design and Implementation of a Counting System for a B2B Marketplace

This article describes the background, requirements, and architectural design of a custom counting system for a B2B second‑hand trading platform, covering dimension definitions, internal vs external implementations, data flow, persistence strategies, code interfaces, and read‑query optimizations.

Zhuanzhuan Tech
Zhuanzhuan Tech
Zhuanzhuan Tech
Design and Implementation of a Counting System for a B2B Marketplace

1 Background

The business of XieKeHui aims to build a B2B second‑hand trading platform with guaranteed transactions, data‑driven user authentication, recommendation and risk‑control capabilities, creating a moat that keeps users engaged.

2 Introduction

Counting systems are ubiquitous in the information‑explosion era, providing real‑time, accurate metrics such as likes, comments, page views, and orders, which help businesses optimize products, marketing strategies, and user experience.

3 Counting Dimensions

3.1 Content‑Based Dimensions

Content: post count, comment count, like count, unread comment/attention/like totals, comment‑like count, reliable/unreliable counts, product view count, negative feedback count.

User: recent activity count, follow count, follower count.

Transaction: recycle order count, inspection count, quotation count, B2B sell/buy counts, product publish/sell/purchase counts.

3.2 Time‑Based Dimensions

Real‑time: most dimensions are counted in real time.

Time‑range: counts for the last N days (e.g., products posted, sold, posts created).

4 Why Build Our Own Counting System

The existing data team provides T+1 delayed data and cannot meet real‑time needs; the middle‑platform offers simple counting but lacks time‑range queries. Therefore, a dedicated counting system is required.

5 Design方案

Two architectural options are considered: an embedded count table with cache for short‑term implementation, and an external, independent counting service for long‑term scalability.

5.1 Embedded Architecture

Uses a count table plus cache. Works well for small scale but suffers from performance bottlenecks, stability risks, and consistency issues as data volume grows.

5.2 External Architecture

Defines four core fields:

Global Counter : stores total value for a business key (e.g., total likes).

Count Stream : records real‑time count events for precise time‑range queries.

Business Type : identifies the counting scenario (e.g., post likes, order sales).

Count Entity : the target object ID (e.g., post ID, order ID).

These fields are also used as Redis keys, enabling fast access and space‑for‑time trade‑offs.

5.2.1 Reporting Process

The reporting flow consists of three steps: data acquisition (via synchronous API calls), data processing (basic validation of business type and entity ID), and persistence (DB write + Redis cache update within a transaction, plus asynchronous MQ for stream records).

public interface BizCountService {
    ZZOpenScfBaseResult<String> reportCount(ReportCountRequest request);
    boolean saveDateOpt(BizCountRecordMsg msg);
    boolean updateDateOpt(HeroBizCountDate heroBizCountDate, BizCountRecordMsg msg);
    ZZOpenScfBaseResult<Long> total(TotalRequest request);
    ZZOpenScfBaseResult<CountBatchResponse> countBatch(CountBatchRequest request);
    ZZOpenScfBaseResult<Long> clear(ClearRequest request);
    ZZOpenScfBaseResult<Long> countTimeBetween(CountTimeBetweenRequest request);
    ZZOpenScfBaseResult<Map<Long, Long>> batchCountTimeBetween(CountTimeBetweenBatchRequest request);
    ZZOpenScfBaseResult<Long> countRecent(CountRecentRequest request);
    ZZOpenScfBaseResult<Map<Long, Long>> batchCountRecent(CountRecentBatchRequest request);
    Long combineTotal(CombineTotalRequest request);
}

The service first validates the request, acquires a distributed lock, updates the total in the DB, sends a MQ message for the stream, and finally syncs the total to Redis.

public ZZOpenScfBaseResult<String> reportCount(ReportCountRequest request) {
    boolean valid = checkAndProcessReportRequest(request);
    if(!valid){
        return ZZOpenScfBaseResult.buildErr(-1,"参数不合法");
    }
    boolean locked = redissionLockHelper.tryLockBizCountTotal(request.getEntityId(), request.getBizType(), () -> {
        heroBizCountTotalManager.saveOrUpdate(request.getEntityId(), request.getBizType(), request.getCount());
    });
    if(!locked){
        log.error("lock failed,request:{}", request);
        return ZZOpenScfBaseResult.buildErr(-1,"获取锁失败");
    }
    try {
        bizCountRecordProducer.sendBizCountRecordMsg(buildRecordMsg(request));
    }catch (Exception e){
        log.error("send report msg error, request:{}", request, e);
    }
    try {
        syncTotal2Cache(request.getBizType(), request.getEntityId());
    }catch (Exception e){
        log.error("sync total from db error, request:{}", request, e);
    }
    return ZZOpenScfBaseResult.buildSucc("");
}

5.2.2 Reading Process

Non‑time‑range queries read from cache first, falling back to DB if needed. Time‑range queries first check for complete natural days; if present, they use the daily aggregation table, otherwise they read directly from the stream table, dramatically reducing the number of rows scanned.

public Map<Long, Long> countTimeBetweenInternal(List<Long> entityIds, BizCountType bizType, Date start, Date end) {
    Map<Long, Long> totalMap = Maps.newHashMapWithExpectedSize(entityIds.size());
    if(!BizCommonDateUtils.containsWholeDays(start, end)){
        return heroBizCountRecordManager.computeRestTime(entityIds, bizType, start, end, Maps.newHashMap());
    }else{
        Map<Long, BizCountDateRange> dateRangeMap = heroBizCountDateManager.getDateRange(entityIds, bizType, start, end);
        Map<Long,Long> dateCountMap = heroBizCountDateManager.countDateBetween(entityIds, bizType, start, end);
        Map<Long,Long> restCountMap= heroBizCountRecordManager.computeRestTime(entityIds, bizType, start, end,dateRangeMap);
        entityIds.forEach(entityId -> {
            Long dateCount = dateCountMap.get(entityId);
            Long restCount = restCountMap.get(entityId);
            if(dateCount != null && restCount != null){
                totalMap.put(entityId, dateCount + restCount);
            }else if(dateCount != null){
                totalMap.put(entityId, dateCount);
            }else if(restCount != null){
                totalMap.put(entityId, restCount);
            }
        });
    }
    return totalMap;
}

6 Summary and Planning

The external counting architecture is industry‑standard and reduces maintenance cost, improves code reuse, and enhances system stability. It also enables space‑for‑time optimization for time‑range queries. Current limitations include DB‑based time‑range queries and lack of read‑heavy/write‑light segregation, which can be addressed in future iterations.

Author: Zhu Hongxu, Java Development Engineer at XieKeHui
backend architectureMicroservicesRedisdata aggregationcounting system
Zhuanzhuan Tech
Written by

Zhuanzhuan Tech

A platform for Zhuanzhuan R&D and industry peers to learn and exchange technology, regularly sharing frontline experience and cutting‑edge topics. We welcome practical discussions and sharing; contact waterystone with any questions.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.