Designing a Scalable Lottery System: Stock Pre‑allocation, Real‑time Risk Control & Dynamic Probability

This article details the design of a reusable, configurable lottery platform for a youth‑focused social app, covering business background, pain points, a modular data model, micro‑service architecture, stock pre‑allocation with optimistic and distributed locks, real‑time risk detection, over‑issue prevention, probability calculation, and future AI‑driven enhancements.

Sohu Tech Products
Sohu Tech Products
Sohu Tech Products
Designing a Scalable Lottery System: Stock Pre‑allocation, Real‑time Risk Control & Dynamic Probability

Business Background

FoxFriend is a social app targeting young users. In its early stage, lottery activities were managed manually through Feed comments, requiring offline prize distribution and making large‑scale, platform‑wide draws impossible.

Business Pain Points

Custom development for each holiday lottery (e.g., Spring Festival) consumes valuable R&D and testing resources and cannot be reused.

Development challenges include long cycles, security, stability, data consistency, chaotic data models, and lack of technical consolidation.

Operations face complex configuration across multiple systems, high communication costs, and delayed activity performance monitoring.

Testing suffers from high cost, repetitive test case creation, and limited manpower.

Solution Design

3.1 Data Model Definition

The core model separates prize tiers, prizes, and gameplay, using loose coupling to support micro‑service architecture with high cohesion and low coupling.

Each lottery can configure multiple prize tiers (e.g., first, second, N‑th) with independent rules such as probability, distribution schedule, and inventory.

Prizes belong to tiers and can be shared across tiers, each with its own rules (limits, stock, distribution).

Gameplay sits at the top layer, decoupled from tiers and prizes, allowing diverse interactive tasks to boost user engagement.

3.2 Business Architecture

The system is divided into several modules:

Gameplay Activity : H5 components let operators select and configure gameplay forms and material information.

Core Business System : Handles activity configuration, C‑end APIs, and data reporting. Includes activity basics, prize configuration, probability rules, inventory, and task rule settings.

Key Module Designs :

Prize configuration – independent prize and tier, supporting single or bundled prize distribution.

Prize management – separate inventory pools for prizes and tiers, enabling flexible allocation and high‑concurrency expansion.

Flexible module composition – supports varied business needs with high cohesion and low coupling.

External System Integration : Integrates task system, user behavior system, and Feed server to provide task orchestration, behavior tracking, and social sharing support.

Business Process

Activity initialization: operators configure basic info, prize tiers, inventory, task rules, set activity period, and publish.

User participation: users complete tasks to earn draw chances, perform the draw, fill prize information (for physical items), and receive the prize.

Backend management: real‑time monitoring, dynamic adjustment of parameters (tasks, probability, inventory), and management of winner lists and prize distribution.

Design Highlights

Pre‑allocated Inventory : Separate total resource pool and activity‑specific pool. Activity reserves stock from the total pool; unused stock returns automatically after the activity ends.

Multi‑tenant Data Isolation : Supports multiple business departments by isolating data per tenant, ensuring security and independent operation.

3.3 Technical Challenges

Key challenges include high‑concurrency stock deduction, stock accuracy, and bucketed design complexity.

Solutions

Bucket design – each activity has an independent stock bucket.

Optimistic locking – version or CAS checks before deduction.

Distributed lock – Redis or ZooKeeper lock for total pool modifications.

Stock pre‑warming – reserve stock before activity starts.

Asynchronous deduction – use message queues for non‑real‑time paths.

Code Example (Stock Service)

@Slf4j
@Service
public class StockService {
    private final PrizeRepository prizeRepository;
    private final ActivityStockRepository activityStockRepository;
    private final RedissonClient redissonClient;
    private final RedisTemplate<String, Object> redisTemplate;
    private final RabbitTemplate rabbitTemplate;

    /**
     * Pre‑allocate stock from total pool to activity pool
     */
    @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)) {
                Prize prize = prizeRepository.findById(prizeId)
                        .orElseThrow(() -> new RuntimeException("Prize not found"));
                if (prize.getAvailableStock() < quantity) {
                    log.warn("Insufficient total stock, prizeId={}, stock={}, needed={}", 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;
            } 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;
        } finally {
            lock.unlock();
        }
    }

    /**
     * Return unused stock to total pool after activity ends
     */
    @Transactional
    public boolean returnUnusedStock(Long activityId) {
        List<ActivityStock> 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)) {
                    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("Returned unused stock, activityId={}, prizeId={}, quantity={}", activityId, stock.getPrizeId(), stock.getStock());
                } 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;
            } finally {
                lock.unlock();
            }
        }
        return true;
    }

    /**
     * Decrease activity stock when a user wins
     */
    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);
        }
        if (remain < 0) {
            redisTemplate.opsForValue().increment(stockKey);
            return false;
        }
        rabbitTemplate.convertAndSend("stock.exchange", "stock.decrease", new StockMessage(activityId, prizeId, 1));
        return true;
    }

    /**
     * Pre‑warm activity stock into Redis
     */
    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‑warm success, activityId={}, prizeId={}, stock={}", activityId, prizeId, activityStock.getStock());
        return true;
    }
}

3.4 Over‑issue Control

To prevent prize over‑issuance, the system uses database transactions, Redis atomic operations, pre‑deduction, asynchronous processing via message queues, and real‑time monitoring with alerts.

3.5 Probability Calculation

Operators configure winning probabilities. The platform ensures actual rates match configurations using SecureRandom, dynamic time‑segment adjustments, reservoir sampling for fixed‑winner scenarios, and real‑time statistics to auto‑tune probabilities.

@Slf4j
@Service
public class LotteryProbabilityService {
    @Autowired private RedisTemplate<String, Object> redisTemplate;
    @Autowired private LotteryActivityRepository activityRepository;
    @Autowired private LotteryPrizeRepository prizeRepository;
    @Autowired private LotteryRecordRepository recordRepository;
    private final Map<Long, ProbabilityStatistics> activityStatisticsMap = new ConcurrentHashMap<>();
    // draw method, probability adjustment, reservoir sampling, etc.
}

Platform Continuous Exploration and Outlook

4.1 Intelligent & Data‑Driven

Dynamic strategy optimization using machine‑learning models to adjust probabilities, inventory allocation, and risk rules based on historical data.

User behavior prediction for load forecasting and resource scheduling.

AI‑powered customer service for prize queries and exception handling.

4.2 Ecosystem Expansion

Open platform supporting cross‑department scenarios such as e‑commerce promotions and content incentives.

Third‑party resource integration for virtual and physical rewards.

Enhanced social virality through deeper Feed integration and team‑based lottery mechanics.

4.3 Operational Efficiency

Zero‑code configuration via visual drag‑and‑drop interfaces for complex rules.

Automated testing tools with mock data generation to reduce manual effort.

Analytics middle‑platform integrating BI for multi‑dimensional activity impact analysis.

Lottery Systembackend architecturedistributed lockingrisk controlstock management
Sohu Tech Products
Written by

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.

0 followers
Reader feedback

How this landed with the community

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.