Coupon Service (Distributed Locks)

Preventing a user from spending the same $100 coupon twice at the exact same time.

The idea

Imagine a user has a single-use $100 coupon. They open your app on their phone and their laptop, and click "Checkout" at the exact same millisecond. Request A hits Server 1, and Request B hits Server 2. Both servers query the Database: "Is coupon used?" Both read false. Both apply the $100 discount, and both set it to true. The user just double-spent their coupon! To stop this, we need a lock that works across multiple servers: a Distributed Lock.

Step 1: Two identical checkout requests arrive at two different Application Servers.

How it works (Redis SETNX)

A standard Mutex only works for threads within a single server. For distributed systems, we use an external fast datastore like Redis. We use the command SETNX (Set if Not eXists). Only the first server to execute this command will succeed in setting the key. The second server will fail, realize the lock is taken, and reject the checkout.

# Applying a Coupon safely using a Redis Lock
def apply_coupon(user_id, coupon_code):
    lock_key = f"lock:coupon:{coupon_code}"
    
    # 1. Try to acquire the distributed lock (expires in 5s)
    lock_acquired = redis.set(lock_key, "locked", nx=True, ex=5)
    
    if not lock_acquired:
        raise Exception("Please wait, transaction in progress.")
        
    try:
        # 2. Critical Section (Safe from concurrent requests)
        status = db.query("SELECT used FROM coupons WHERE code=?", coupon_code)
        if status == 'used':
            raise Exception("Coupon already used!")
            
        db.execute("UPDATE coupons SET used=true WHERE code=?", coupon_code)
        checkout()
    finally:
        # 3. Always release the lock!
        redis.delete(lock_key)

Cost

Distributed locks require a network hop to Redis before every critical action, adding a few milliseconds of latency. Additionally, they introduce failure modes: what if Server A acquires the lock, then Server A crashes before releasing it? You MUST set an expiration TTL (Time-To-Live) on the Redis key, or the lock will be held forever.

Watch out for