Distributed Coupon Redemption System — Architecture of a Race Condition Proof Microservice
How to build a high-concurrency coupon redemption system with Spring Boot, Redis distributed locking, Nginx load balancing, and React — and…
Distributed Coupon Redemption System — Architecture of a Race Condition Proof Microservice
How to build a high-concurrency coupon redemption system with Spring Boot, Redis distributed locking, Nginx load balancing, and React — and watch the race condition disappear when you flip a config flag.
Learn how to build a production-grade distributed coupon redemption system using Spring Boot microservices, Redis distributed locks, Nginx load balancing, and React. Complete architecture deep dive with code, diagrams, and real race condition debugging.
Imagine a flash sale. A coupon code SUMMER50 has exactly 100 redemptions. One thousand users click "Claim" simultaneously. Your system has three application instances running behind a load balancer. What happens?
Full story for non-members | Instagram-@arvind.codefarm | E-Books on Java/Microservices/Springboot | Whatsapp Group

The Problem: 100 Coupons, 1000 Users, Zero Coordination
Without distributed locking — the race condition:

This is a race condition — multiple instances read and write the same shared state without coordination. The result: overselling, negative inventory, inconsistent database state, and real business losses.
With distributed locking — coordinated access:

This project demonstrates both scenarios side by side — with a single config toggle to switch between them. Zero code changes.
System Architecture Overview

Component Responsibilities

The Lock Strategy Pattern — No Code Changes Needed
The project’s most important design decision: the lock is pluggable.

public interface LockStrategy {
boolean acquireLock(String key, String value, long timeoutMs);
void releaseLock(String key, String value);
}
NoLockStrategy (Race Condition Mode)
public class NoLockStrategy implements LockStrategy {
@Override
public boolean acquireLock(String key, String value, long timeoutMs) {
return true;
}
@Override
public void releaseLock(String key, String value) {
// No-op
}
}
Every request passes through without waiting. Race conditions guaranteed.
RedisLockStrategy (Safe Mode)
public class RedisLockStrategy implements LockStrategy {
@Override
public boolean acquireLock(String key, String value, long timeoutMs) {
Boolean acquired = redisTemplate.opsForValue()
.setIfAbsent(key, value, Duration.ofMillis(timeoutMs));
return Boolean.TRUE.equals(acquired);
}
@Override
public void releaseLock(String key, String value) {
redisTemplate.execute(unlockScript, List.of(key), value);
}
}
Uses Redis SET NX (set if not exists) with a TTL to create a lease.
Configuration-Driven Selection
@Configuration
public class LockConfig {
@Bean
@ConditionalOnProperty(name = "coupon.lock.enabled", havingValue = "true", matchIfMissing = true)
public LockStrategy redisLockStrategy(StringRedisTemplate redisTemplate) {
return new RedisLockStrategy(redisTemplate);
}
@Bean
@ConditionalOnProperty(name = "coupon.lock.enabled", havingValue = "false")
public LockStrategy noLockStrategy() {
return new NoLockStrategy();
}
}
Set coupon.lock.enabled=true or
coupon.lock.enabled=false in application.yaml.
That's it. No code changes.
The Critical Bug We Found (And Fixed)
During development, discovered a subtle but devastating bug: the Redis lock was being released before the database transaction committed.

The Fix: Lock Outside the Transaction

// FIXED: Lock acquired before transaction, released after
public RedeemResponse redeemCoupon(RedeemRequest request) {
boolean acquired = acquireLockWithRetry(lockKey, lockValue, ...);
if (!acquired) return RedeemResponse.failure("Busy", instanceName);
try {
return transactionTemplate.execute(status -> {
Coupon coupon = couponRepository.findByCode(request.couponCode()).orElse(null);
if (coupon == null) return RedeemResponse.failure("Not Found", instanceName);
if (coupon.getRemainingRedemptions() <= 0) {
redemptionRepository.save(new CouponRedemption(coupon.getId(), request.username(), RedemptionStatus.FAILED));
return RedeemResponse.failure("Coupon Exhausted", instanceName);
}
coupon.setRemainingRedemptions(coupon.getRemainingRedemptions() - 1);
redemptionRepository.save(new CouponRedemption(coupon.getId(), request.username(), RedemptionStatus.SUCCESS));
return RedeemResponse.success("Coupon Redeemed", instanceName);
});
} finally {
lockStrategy.releaseLock(lockKey, lockValue);
}
}
Fixed order:
- [OK] Lock acquired (Redis)
- [OK] Transaction begins (MySQL connection)
- [OK] JPA operations
- [OK] Transaction commits (flush to MySQL)
- [OK] Lock released — next request sees fresh data
- [OK] No race condition
REST API Contract

Example: Redeem a Coupon
curl -X POST http://localhost/api/coupons/redeem \
-H "Content-Type: application/json" \
-d '{"couponCode": "SUMMER50", "username": "Rahul"}'
Success response:
{
"success": true,
"message": "Coupon Redeemed",
"instanceName": "coupon-app-2"
}
Exhausted response:
{
"success": false,
"message": "Coupon Exhausted",
"instanceName": "coupon-app-1"
}
Running the Demo
docker compose up --build -d
curl http://localhost/health
curl -X POST http://localhost/api/coupons \
-H "Content-Type: application/json" \
-d '{"code": "SUMMER50", "totalRedemptions": 100}'
cd frontend-react && npm install && npm run dev
Open http://localhost:5173, click Claim x100, and watch the real-time results.
What’s Next — Deep Dives
This article covered the high-level architecture. Each component has its own deep dive:

Key Takeaways
- Race conditions in distributed systems are real — they happen when multiple instances share state without coordination
- Redis distributed locking is the fix —
SET NXwith TTL and a Lua unlock script provides safe coordination - Lock timing matters — releasing the lock before the transaction commits still allows races (we fixed this bug)
- The Strategy Pattern makes it configurable — switch between lock and no-lock with a single config property, zero code changes
- Docker Compose makes complex demos simple — 6 services, one command
Source Code
The complete source code for this project is available on GitHub:
https://github.com/codefarm0/coupon-redemption-system
Clone it, run docker compose up, and see the race condition disappear when you flip coupon.lock.enabled=true.
Liked this deep dive story? If Yes Please 👏 Clap(50) | 📤 Share | 🔔 Follow
Below is a collection of all related stories in one place
https://codefarm0.medium.com/list/coupon-redemption-system-full-stack-project-b97840f224e7
메타데이터
- post_id
- cbaca0f9c070
- slug
- distributed-coupon-redemption-system-architecture-of-a-race-condition-proof-microservice-cbaca0f9c070
- url
- https://medium.com/@codefarm0/distributed-coupon-redemption-system-architecture-of-a-race-condition-proof-microservice-cbaca0f9c070
- canonical_url
- https://medium.com/@codefarm0/distributed-coupon-redemption-system-architecture-of-a-race-condition-proof-microservice-cbaca0f9c070
- author_url
- https://medium.com/@codefarm0
- status
- ok
- fetched_at
- 2026-08-09 18:14:25