System Design: Coupon and Promo Code System — Validation, Redemption, and Abuse Prevention

Core Requirements

A coupon system allows businesses to issue discount codes that customers apply at checkout. Types: percentage discount (20% off), fixed amount ($10 off), free shipping, buy-one-get-one (BOGO). Constraints: one-time use codes (each code works once for any customer), per-customer limits (use at most once per account), minimum order value, expiration dates, product/category restrictions. At scale: Black Friday sees millions of coupon validations in minutes — the system must handle burst validation traffic without double-redemptions.

Data Model

CouponCampaign: campaign_id, name, discount_type (PERCENT, FIXED, FREE_SHIPPING), discount_value, min_order_value, max_discount_cap, start_date, end_date, total_redemption_limit (nullable), per_customer_limit, applicable_categories (JSON), status (ACTIVE, PAUSED, EXPIRED). CouponCode: code (PK, unique string), campaign_id, code_type (UNIVERSAL=one code for all customers, UNIQUE=one-time single-use), is_used (for unique codes), used_by_user_id, used_at. Redemption: redemption_id, code, user_id, order_id, discount_applied, redeemed_at.

Validation and Redemption

Validation is read-heavy; redemption is write-critical (must be atomic). Validation checks: (1) Code exists and belongs to an active campaign. (2) Campaign is within start_date and end_date. (3) Order total >= min_order_value. (4) User has not exceeded per_customer_limit: SELECT COUNT(*) FROM redemptions WHERE code=:c AND user_id=:u < per_customer_limit. (5) Campaign total redemptions not exceeded: SELECT COUNT(*) FROM redemptions WHERE campaign_id=:id < total_redemption_limit. (6) For UNIQUE codes: is_used = FALSE. Redemption (at order confirmation): must be atomic to prevent double-use:

def redeem_coupon(code, user_id, order_id, discount):
    with db.transaction():
        coup = db.query("SELECT * FROM coupon_codes WHERE code=%s FOR UPDATE", code)
        # Re-validate inside transaction
        if coup.code_type == "UNIQUE" and coup.is_used:
            raise CouponAlreadyUsedError()
        count = db.query(
            "SELECT COUNT(*) FROM redemptions WHERE code=%s AND user_id=%s",
            code, user_id
        )
        if count >= coup.campaign.per_customer_limit:
            raise LimitExceededError()
        db.execute("INSERT INTO redemptions VALUES (%s,%s,%s,%s,%s,NOW())",
                   uuid4(), code, user_id, order_id, discount)
        if coup.code_type == "UNIQUE":
            db.execute("UPDATE coupon_codes SET is_used=TRUE, used_by=%s WHERE code=%s",
                       user_id, code)

Bulk Code Generation

For campaigns needing thousands of unique codes (loyalty rewards, promotional mailers): generate offline in bulk. Algorithm: generate random 8-character alphanumeric codes (exclude confusable chars: 0/O, 1/I/l). Batch insert into coupon_codes with a unique index — retry any collisions. For 1 million codes: generate in batches of 10,000, insert with ON CONFLICT DO NOTHING, track how many were actually inserted (some may collide), re-generate the missing ones. Total collisions at 1M codes from a space of 36^8 = 2.8 trillion: negligibly small. Store codes in a database with an index on (code). Caching: cache active campaign metadata in Redis — avoid a DB lookup on every validation request. Invalidate on campaign updates.

Abuse Prevention

Coupon abuse patterns: (1) Multi-accounting: user creates multiple accounts to reuse a per-customer code. Mitigation: fingerprint by device ID, payment method, shipping address. Flag accounts with the same fingerprint. (2) Code sharing: unique codes leaked publicly and used by unintended recipients. Mitigation: issue codes via personalized delivery (email to the specific user) rather than broadcasting. Bind codes to the recipient’s email at generation time (validate that user’s email matches the code’s recipient). (3) Bulk redemption attacks: bots try thousands of code combinations. Mitigation: rate limit validation attempts per IP and per user (max 10 failed validations per minute). Add CAPTCHA after 3 failed attempts. (4) Order manipulation: user applies coupon, then edits the cart to reduce it below min_order_value. Mitigation: re-validate coupon at payment, not just at cart stage.

Asked at: Shopify Interview Guide

Asked at: Stripe Interview Guide

Asked at: Airbnb Interview Guide

Asked at: DoorDash Interview Guide

Scroll to Top