Design and Implementation of a Scalable Lottery Activity Platform
The article describes how the FoxFriend team built a scalable, configurable lottery‑activity platform that replaces manual feed‑based draws with a modular micro‑service architecture, featuring a flexible prize‑tier data model, pre‑occupied inventory buckets, multi‑tenant isolation, high‑concurrency stock deduction, user risk controls, accurate probability handling, and a roadmap toward AI‑driven optimization.
Background: "FoxFriend" is a social app targeting young users. In its early stage, lottery activities were managed manually through Feed comments, which was cumbersome and could not support large‑scale, cross‑platform draws.
Pain points: Custom development for each holiday draw required long cycles, duplicated effort, and high testing cost. Developers faced long development cycles, chaotic data models, and difficulty accumulating reusable technology. Operations suffered from fragmented configuration, high communication cost, and lack of real‑time monitoring. Testers had to rewrite test cases for each activity, leading to high labor cost.
Requirements: Build a generic, flexible, configurable lottery‑activity platform that allows operators to set up activities without code, ensures reusability and scalability of the technical architecture, and reduces future development and testing workload.
Solution Design
Data Model (Section 3.1): The core model consists of three loosely coupled entities – prize tier, prize, and gameplay. Each tier can have multiple prizes, each prize can belong to multiple tiers, and gameplay is decoupled from prize/prize‑tier, enabling modular micro‑service design.
Architecture (Section 3.2): The system is divided into three main parts:
Business gameplay activities – H5 front‑end provides configurable gameplay components for operators.
Core business system – Handles activity configuration, C‑end APIs, data reporting, and includes modules for activity basics, prize configuration, inventory, and task rules.
External system integration – Task system, user‑behavior system, and Feed server provide essential support for tasks, behavior tracking, and social sharing.
Key design points:
Pre‑occupy inventory: Separate total resource pool and activity‑specific pool; inventory is pre‑reserved for each activity and returned to the total pool after the activity ends.
Multi‑tenant data isolation: Different business units operate on isolated data to ensure security.
Technical Challenges (Section 3.3)
1. Bucket‑based inventory deduction under high concurrency.
2. User risk control to prevent cheating (scripts, multiple accounts).
3. Over‑issue control to avoid issuing more prizes than available.
4. Accurate probability calculation and dynamic adjustment.
Challenge 1 – Bucket Inventory Deduction
Solution: Use per‑activity inventory buckets, distributed locks (Redis/ZooKeeper) for total pool updates, optimistic locking for DB updates, and asynchronous processing via message queues.
Example implementation (core logic):
@Slf4j
@Service
public class StockService {
private final PrizeRepository prizeRepository;
private final ActivityStockRepository activityStockRepository;
private final RedissonClient redissonClient;
private final RedisTemplate
redisTemplate;
private final RabbitTemplate rabbitTemplate;
@Transactional
public boolean preOccupyStock(Long activityId, Long prizeId, int quantity) {
RLock lock = redissonClient.getLock("stock:total:" + prizeId);
try {
if (lock.tryLock(5, 30, TimeUnit.SECONDS)) {
try {
Prize prize = prizeRepository.findById(prizeId)
.orElseThrow(() -> new RuntimeException("Prize not found"));
if (prize.getAvailableStock() < quantity) {
log.warn("Insufficient total stock, prizeId={}, current={}, required={}",
prizeId, prize.getAvailableStock(), quantity);
return false;
}
int updated = prizeRepository.decreaseStock(prizeId, quantity, prize.getVersion());
if (updated == 0) {
log.warn("Optimistic lock failed, prizeId={}, version={}", prizeId, prize.getVersion());
return false;
}
ActivityStock activityStock = activityStockRepository
.findByActivityIdAndPrizeId(activityId, prizeId)
.orElse(new ActivityStock(activityId, prizeId, 0));
activityStock.setStock(activityStock.getStock() + quantity);
activityStockRepository.save(activityStock);
log.info("Pre‑occupy success, activityId={}, prizeId={}, quantity={}",
activityId, prizeId, quantity);
return true;
} finally {
lock.unlock();
}
} else {
log.warn("Lock timeout, activityId={}, prizeId={}", activityId, prizeId);
return false;
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.error("Pre‑occupy interrupted", e);
return false;
}
}
@Transactional
public boolean returnUnusedStock(Long activityId) {
List
stocks = activityStockRepository.findByActivityId(activityId);
for (ActivityStock stock : stocks) {
if (stock.getStock() <= 0) continue;
RLock lock = redissonClient.getLock("stock:total:" + stock.getPrizeId());
try {
if (lock.tryLock(5, 30, TimeUnit.SECONDS)) {
try {
Prize prize = prizeRepository.findById(stock.getPrizeId())
.orElseThrow(() -> new RuntimeException("Prize not found"));
prize.setAvailableStock(prize.getAvailableStock() + stock.getStock());
prizeRepository.save(prize);
stock.setStock(0);
activityStockRepository.save(stock);
log.info("Return unused stock success, activityId={}, prizeId={}, quantity={}",
activityId, stock.getPrizeId(), stock.getStock());
} finally {
lock.unlock();
}
} else {
log.warn("Lock timeout during return, activityId={}, prizeId={}", activityId, stock.getPrizeId());
return false;
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.error("Return interrupted", e);
return false;
}
}
return true;
}
public boolean decreaseActivityStock(Long activityId, Long prizeId) {
String stockKey = "stock:activity:" + activityId + ":" + prizeId;
Long remain = redisTemplate.opsForValue().decrement(stockKey);
if (remain == null) {
return preloadActivityStock(activityId, prizeId);
} else if (remain < 0) {
redisTemplate.opsForValue().increment(stockKey);
return false;
}
rabbitTemplate.convertAndSend("stock.exchange", "stock.decrease",
new StockMessage(activityId, prizeId, 1));
return true;
}
public boolean preloadActivityStock(Long activityId, Long prizeId) {
ActivityStock activityStock = activityStockRepository
.findByActivityIdAndPrizeId(activityId, prizeId)
.orElseThrow(() -> new RuntimeException("Activity stock not found"));
String stockKey = "stock:activity:" + activityId + ":" + prizeId;
redisTemplate.opsForValue().set(stockKey, activityStock.getStock());
log.info("Pre‑heat activity stock success, activityId={}, prizeId={}, stock={}",
activityId, prizeId, activityStock.getStock());
return true;
}
}Challenge 2 – User Risk Control
Solution: Analyze user behavior (login frequency, device switches), apply device fingerprinting, IP limits, captchas, and a configurable rule engine that supports per‑activity limits, blacklists, and real‑time monitoring.
Challenge 3 – Over‑Issue Control
Solution: Use database transactions, Redis atomic DECR/INCR, pre‑deduction of inventory, asynchronous processing via MQ, and monitoring/alerting when stock is low.
Challenge 4 – Probability Calculation
Solution: Employ SecureRandom for high‑quality randomness, dynamic probability adjustment based on activity progress and inventory consumption, reservoir sampling for fixed‑winner scenarios, and real‑time statistics to keep actual win rates close to configured values.
Key probability service snippet:
@Slf4j
@Service
public class LotteryProbabilityService {
@Autowired private RedisTemplate
redisTemplate;
@Autowired private LotteryActivityRepository activityRepository;
@Autowired private LotteryPrizeRepository prizeRepository;
@Autowired private LotteryRecordRepository recordRepository;
private final Map
activityStatisticsMap = new ConcurrentHashMap<>();
public Long draw(Long activityId, String userId) {
LotteryActivity activity = activityRepository.findById(activityId)
.orElseThrow(() -> new RuntimeException("Activity not found"));
if (!isActivityInProgress(activity)) {
throw new RuntimeException("Activity not started or ended");
}
List
prizes = prizeRepository.findByActivityId(activityId);
if (prizes.isEmpty()) {
throw new RuntimeException("No prizes configured");
}
ProbabilityStatistics stats = activityStatisticsMap.computeIfAbsent(activityId,
k -> new ProbabilityStatistics(activityId));
adjustProbabilityByStage(activity, prizes, stats);
long seed = generateSeed(userId, activityId);
SecureRandom random = new SecureRandom();
random.setSeed(seed);
Long prizeId = doDraw(random, prizes, stats);
recordDrawResult(activityId, userId, prizeId, stats);
return prizeId;
}
// ... (methods for adjustProbabilityByStage, normalizeProbabilities, calculateActivityProgress, etc.)
}Future Outlook (Section 4): The platform will evolve toward intelligent, data‑driven optimization (machine‑learning‑based probability and inventory tuning), multi‑tenant ecosystem expansion, zero‑code configuration, automated testing tools, and integrated BI for effect analysis.
Sohu Tech Products
A knowledge-sharing platform for Sohu's technology products. As a leading Chinese internet brand with media, video, search, and gaming services and over 700 million users, Sohu continuously drives tech innovation and practice. We’ll share practical insights and tech news here.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.