Backend Development 12 min read

Code Review of Coupon Claim and Approval Workflow: Issues and Optimization Strategies

This article reviews Java Spring code for a coupon claim and approval workflow, identifies concurrency and transaction issues such as missing locks and inconsistent update order, and proposes optimizations including user‑level locking, simplifying lock management, and aligning transactional update sequences to prevent deadlocks.

Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Code Review of Coupon Claim and Approval Workflow: Issues and Optimization Strategies

In this technical review, the author examines a Java Spring implementation for a coupon claim feature and an approval business process, highlighting several concurrency and transactional problems. The discussion begins with a festive preface and proceeds to the problematic code sections, including the controller and service layers.

Original Controller Logic

@RedisRateLimiter(value = 200, limit = 1)
@PostMapping(value = "/claim")
public Object claim(@RequestBody EquityClaimReqEx claimReqEx) {
    // 1. User validation
    Result
result = claimService.check(claimReqEx);
    if (!result.isSuccess()) {
        return result;
    }
    // 2. Claim coupon – separate transaction
    Result
claimCore = claimService.claimTran(claimReqEx);
    if (!claimCore.isSuccess()) {
        return claimCore;
    }
    // 3. Record claim details
    return claimService.record(claimReqEx);
}

The controller lacks any locking mechanism, which may lead to duplicate claims under concurrent requests.

Original Service Core Logic

@Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.READ_COMMITTED, rollbackFor = RuntimeException.class)
public Result
claimCoreTransactional2(ActivityEquityClaimReqEx claimReq) {
    long startTimeCore = System.currentTimeMillis();
    // Step 1: Randomly fetch an unissued coupon code
    EquityGoods equityGoods = equityGoodsDao.findByClaim(claimReq.getActivityId(), claimReq.getEquityId());
    if (equityGoods == null) {
        return Result.error("权益商品异常:无可领取权益商品!");
    }
    // Step 2: Acquire lock on the coupon code using Redis setnx
    Boolean acquired = redisTemplate.opsForValue().setIfAbsent(equityGoods.getRedeemCode(), "Lock", Duration.ofSeconds(60 * 5));
    if (acquired) {
        try {
            // Step 3: Lock inventory via custom lock manager
            ClainReentrantLock claimLock = claimLockManager.getLockForClaim(getLockKey(claimReq), 60 * 24, TimeUnit.MINUTES);
            claimLock.lock();
            // Step 4: Query and decrement inventory
            Inventory inventory = inventoryDao.findByconfirm(claimReq);
            boolean isOk = inventoryDao.reduceInventory(inventory.getId(), isCompleted);
            if (!isOk) {
                throw new RuntimeException("更新配置权益核销状态失败!");
            }
            // Step 5: Update coupon status and user record
            Boolean ret = updateUserRecordAndRedeemCode();
            if (!ret) {
                throw new RuntimeException("对不起,系统繁忙,稍后再试试吧!");
            }
        } finally {
            claimLock.unlock();
            redisTemplate.delete(equityGoods.getRedeemCode());
            long endTimeCore = System.currentTimeMillis();
            logger.info("================领取逻辑耗时测试. Timed :{}", endTimeCore - startTimeCore);
        }
        return Result.ok();
    } else {
        return Result.error("对不起,系统繁忙,稍后再试试吧!");
    }
}

The service layer mixes custom lock management with database transactions, leading to potential deadlocks and race conditions, especially because the lock is released before the transaction commits.

Identified Problems

The rate‑limiter annotation implementation is flawed.

The controller performs user validation without any locking, risking duplicate claims.

Using limit 1 in SQL to fetch a coupon can cause many users to receive the same code.

Custom lock utilities are hard to maintain and may be released prematurely.

Nested try blocks increase complexity and error‑handling difficulty.

Locks inside a transaction can be released before the transaction finishes, creating concurrency gaps.

Optimization Proposals for Coupon Claim

Apply a user‑level lock in the controller (e.g., using Redisson) and fail fast if the lock cannot be acquired.

Rely on the database's row‑level locking for inventory decrement and coupon status updates, removing custom lock code.

Fetch coupon codes from a thread‑safe queue or Redis set instead of using limit 1 queries.

Ensure the update order of tables is consistent across all transactional operations to avoid deadlocks.

Revised controller snippet:

@RedisRateLimiter(value = 200, limit = 1)
@PostMapping(value = "/claim")
public Object claim(@RequestBody EquityClaimReqEx claimReqEx) {
    Boolean lock = redissonLockClient.tryLock(RedisKeys.COUPON_RECEIVE_LOCK + user.getId(), 20);
    if (!lock) {
        return "服务繁忙.....";
    }
    try {
        Result
result = claimService.check(claimReqEx);
        if (!result.isSuccess()) {
            return result;
        }
        Result
claimCore = claimService.claimTran(claimReqEx);
        if (!claimCore.isSuccess()) {
            return claimCore;
        }
        return claimService.record(claimReqEx);
    } finally {
        redissonLockClient.unlock(RedisKeys.COUPON_RECEIVE_LOCK + user.getId());
    }
}

Approval Business Review

The approval workflow consists of three operations: submit approval, approve, and cancel approval. The original code updates the log table first and then the main table during approval, while the cancel operation updates the main table before the log table, leading to inconsistent lock acquisition order.

Potential deadlock scenario:

Two concurrent threads invoke approve and approveCancel on the same business record.

Each thread holds a lock on a different table and waits for the other, causing a deadlock.

Optimization for Approval Workflow

Standardize the update order across all transactional methods (e.g., always update the main table first, then the log table) to keep lock acquisition consistent.

Consider reducing the scope of transactions or using optimistic locking where appropriate.

Conclusion

The article presented two case studies—coupon claim and approval/cancel business—demonstrating that improper lock usage and inconsistent transactional update sequences can cause concurrency bugs and deadlocks. By applying user‑level locking, simplifying lock management, and aligning update orders, the reliability and performance of backend services can be significantly improved.

JavaTransactionconcurrencySpringCode ReviewDistributed Lock
Rare Earth Juejin Tech Community
Written by

Rare Earth Juejin Tech Community

Juejin, a tech community that helps developers grow.

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.