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.

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