Coupon Service Low-Level Design: Code Generation, Redemption Validation, and Usage Limits

Coupon Schema

A coupon service manages promotional codes that apply discounts to orders. The core schema:

CREATE TABLE coupons (
  coupon_id          UUID PRIMARY KEY,
  code               VARCHAR(32) UNIQUE NOT NULL,
  type               ENUM('percent_off','fixed_amount','free_shipping','bogo') NOT NULL,
  value              INTEGER,        -- pct (0-100) or cents for fixed_amount
  currency           CHAR(3),
  max_uses_total     INTEGER,        -- NULL = unlimited
  max_uses_per_user  INTEGER NOT NULL DEFAULT 1,
  min_order_value    INTEGER,        -- cents, NULL = no minimum
  applicable_products JSONB,         -- NULL = all products
  start_date         TIMESTAMPTZ,
  expires_at         TIMESTAMPTZ,
  status             ENUM('active','paused','exhausted','expired') NOT NULL DEFAULT 'active'
);

CREATE TABLE coupon_redemptions (
  redemption_id UUID PRIMARY KEY,
  coupon_id     UUID NOT NULL REFERENCES coupons(coupon_id),
  user_id       BIGINT NOT NULL,
  order_id      VARCHAR(128) NOT NULL,
  discount_amount INTEGER NOT NULL,  -- actual discount applied in cents
  redeemed_at   TIMESTAMPTZ NOT NULL DEFAULT now(),
  UNIQUE (coupon_id, user_id)        -- enforce per-user limit at DB level
);

Code Generation

Coupon codes must be unique, unguessable, and human-friendly. Use base58 encoding (excludes ambiguous characters: 0/O, 1/l/I) to generate random codes:

BASE58_CHARS = "23456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz"

function generate_code(length=8):
    return ''.join(random.choice(BASE58_CHARS) for _ in range(length))

8-character base58 gives 58^8 ≈ 128 trillion combinations. Collision probability is negligible even with millions of codes. Always check for uniqueness via DB unique constraint before returning the code.

For bulk campaign generation (e.g., 100,000 codes for email blast): batch-insert with ON CONFLICT DO NOTHING and retry any collisions. Generating and storing 100K codes takes under a second.

Redemption Validation Steps

Validate in this order before applying any discount:

  1. Code exists: Look up by code. Return generic “invalid code” error if not found (do not reveal whether code exists).
  2. Status active: Reject if status is paused, exhausted, or expired.
  3. Not expired: Check expires_at > now() and start_date <= now().
  4. Global usage limit: SELECT COUNT(*) FROM coupon_redemptions WHERE coupon_id=? — reject if >= max_uses_total.
  5. Per-user limit: Check SELECT COUNT(*) FROM coupon_redemptions WHERE coupon_id=? AND user_id=? — reject if >= max_uses_per_user.
  6. Minimum order value: Reject if cart total < min_order_value.
  7. Applicable products: If applicable_products is set, verify at least one cart item is in the eligible list.

Atomic Redemption

Two users applying the same coupon simultaneously can both pass validation and both redeem, exceeding usage limits. Prevent this with atomicity:

Database approach (preferred): Insert into coupon_redemptions with the unique constraint on (coupon_id, user_id). The unique constraint enforces per-user limits at the database level. For global limits, use a database-level counter with an optimistic lock:

BEGIN;
SELECT usage_count FROM coupons WHERE coupon_id=? FOR UPDATE;
-- check usage_count < max_uses_total
INSERT INTO coupon_redemptions (...);
UPDATE coupons SET usage_count = usage_count + 1 WHERE coupon_id=?;
COMMIT;

Redis approach: For high-throughput scenarios, use SETNX coupon:{code}:lock:{user_id} 1 EX 30 to acquire a short-lived lock before the DB transaction. Prevents redundant DB work from concurrent requests.

Discount Application

  • percent_off: discount = order_total * (value / 100.0). Cap at order total.
  • fixed_amount: discount = MIN(order_total, value). Cannot exceed order total.
  • free_shipping: Discount equals the shipping cost line item. Zero if shipping is already free.
  • BOGO (Buy One Get One): Identify the cheapest qualifying item in the cart, set its effective price to zero. Requires sorting eligible items by price ascending and zeroing the first one.

Always record the actual discount_amount applied (in cents) in the redemption record, not just the coupon value. The applied discount may differ from the nominal value (e.g., a $20 coupon applied to a $15 order).

Single-Use Codes

Some campaigns require a unique code per recipient (e.g., referral codes, personalized offers). These differ from campaign bulk codes:

  • Generated on demand at the time the offer is issued to a specific user.
  • max_uses_total=1 and max_uses_per_user=1 — effectively single-use by any user.
  • Often pre-linked to a user for attribution, but can be transferred (friend gives code to friend).

Stacking Rules and Analytics

Stacking policy: by default, only one coupon per order. Enforce this by checking for existing coupon on the cart before applying a new one. Some programs allow stacking (e.g., loyalty discount + promotional code) — implement with a stackable flag on the coupon and a stack priority to determine application order.

Coupon performance analytics: track redemption rate (redemptions / codes_distributed), average discount per order, total revenue impact, and fraud signals (same user, multiple accounts). Surface these metrics in an internal dashboard for marketing teams to evaluate campaign effectiveness.

{
“@context”: “https://schema.org”,
“@type”: “FAQPage”,
“mainEntity”: [
{
“@type”: “Question”,
“name”: “How is atomic coupon redemption implemented to prevent double-use?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “Redemption uses an atomic conditional update — UPDATE coupons SET status='redeemed', redeemed_by=?, redeemed_at=NOW() WHERE code=? AND status='active' — and checks that exactly one row was affected; if zero rows are affected the coupon is already redeemed or invalid. For Redis-backed implementations, a Lua script performs the check-and-set atomically within a single server-side execution, preventing race conditions under concurrent redemption attempts.”
}
},
{
“@type”: “Question”,
“name”: “How are per-user usage limits enforced concurrently?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “A Redis counter keyed by coupon_id:user_id is incremented with INCR and compared against the limit in a Lua script before the redemption record is written; the counter is set with an expiry aligned to the coupon's validity window so it auto-cleans. Alternatively, a database-level unique partial index on (coupon_id, user_id) where usage_count < limit enforces the constraint durably without a separate counter."
}
},
{
"@type": "Question",
"name": "How are bulk single-use coupon codes generated?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Codes are generated as cryptographically random byte sequences (e.g., 12 bytes from CSPRNG) encoded to a URL-safe Base32 or alphanumeric string, ensuring global uniqueness without coordination; they are bulk-inserted into the database in batches with a unique index on the code column to catch the astronomically rare collision. A checksum character appended to each code lets point-of-sale systems detect transcription errors before a round trip to the server."
}
},
{
"@type": "Question",
"name": "How is coupon fraud detected?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Anomaly detection rules flag patterns such as a single account redeeming many unique single-use codes in a short window, geographically dispersed redemptions for codes that should have been distributed to a specific region, or velocity spikes in redemption rate for a campaign. Flagged events are queued for review and the associated accounts or codes can be soft-blocked pending investigation without immediately impacting legitimate users."
}
}
]
}

See also: Scale AI Interview Guide 2026: Data Infrastructure, RLHF Pipelines, and ML Engineering

See also: Shopify Interview Guide

See also: Stripe Interview Guide 2026: Process, Bug Bash Round, and Payment Systems

See also: Uber Interview Guide 2026: Dispatch Systems, Geospatial Algorithms, and Marketplace Engineering

Scroll to Top