Low Level Design: Coupon Distribution Service

A coupon distribution service needs to handle bulk code generation, targeted delivery, safe redemption, and fraud prevention at scale. This article covers the low-level design of each layer.

Coupon Schema

Each coupon code maps to a campaign and carries its own usage constraints:

CREATE TABLE coupon (
  code           VARCHAR(20) PRIMARY KEY,
  campaign_id    BIGINT NOT NULL,
  discount_type  ENUM('pct','fixed') NOT NULL,
  discount_value DECIMAL(10,2) NOT NULL,
  min_order      DECIMAL(10,2) NOT NULL DEFAULT 0,
  max_uses       INT NOT NULL DEFAULT 1,
  uses_count     INT NOT NULL DEFAULT 0,
  expires_at     DATETIME NOT NULL,
  status         ENUM('active','exhausted','expired','revoked') NOT NULL DEFAULT 'active'
);

A separate coupon_redemption table logs every successful use with user_id, order_id, redeemed_at, and the idempotency_key.

Coupon Pool Generation

Codes are pre-generated in batch before a campaign launches. The generator:

  • Uses nanoid or a Base36 encoder over a cryptographic random source to produce N unique codes of fixed length (e.g., 10 characters)
  • Checks for collisions against existing codes before bulk insert
  • Inserts the full batch in a single transaction to avoid partial pools
  • Stores the pool in a Redis SET for O(1) pop during distribution: SPOP campaign:{id}:pool

Pre-generation keeps distribution latency low — no code is minted at request time.

Targeted Distribution

Each user in a campaign segment receives exactly one code. The distribution flow:

  1. Pop one code from the Redis pool for the campaign
  2. Write a user_coupon row linking user_id to the code (unique constraint on campaign_id, user_id)
  3. Send the code via email or push notification with personalized messaging
  4. If the DB write fails (duplicate), return the code to the pool and skip the user

The unique constraint on (campaign_id, user_id) enforces one code per user per campaign at the database level, regardless of how many times the distribution job is retried.

Redemption Flow

At checkout, the redemption service validates and applies the coupon atomically:

  1. Validate code: fetch from DB, confirm status is active
  2. Check expiry: reject if expires_at is in the past
  3. Check usage cap: reject if uses_count >= max_uses
  4. Check minimum order: reject if cart total is below min_order
  5. Atomic increment: UPDATE coupon SET uses_count = uses_count + 1 WHERE code = ? AND uses_count < max_uses — if 0 rows affected, the code was just exhausted by a concurrent request
  6. Apply discount and confirm the order

The conditional UPDATE is the critical guard against race conditions under concurrent redemptions of a shared code.

Idempotency

Each redemption request includes a client-supplied idempotency_key (typically order_id). Before processing, the service checks the coupon_redemption table for an existing row with that key. If found, the previous result is returned without re-applying the discount. This prevents double-application on network retries.

Fraud Detection

A background job scans redemption logs for abuse patterns:

  • Multi-code abuse: same user redeeming more than one code from the same campaign (detectable via join on user_coupon and coupon_redemption)
  • Velocity anomalies: unusually high redemption rate from a single IP or device fingerprint
  • Account farming: clusters of new accounts redeeming codes within minutes of registration

Flagged accounts trigger a manual review queue and automatic code revocation for the suspected set.

Expiry Job and Analytics

A scheduled job runs hourly to mark expired codes: UPDATE coupon SET status = 'expired' WHERE expires_at < NOW() AND status = 'active'. This keeps the active pool clean without relying solely on application-layer checks.

Per-campaign analytics expose: codes distributed, codes redeemed, redemption rate, average discount applied, and revenue attributed. These feed into A/B testing of discount levels and targeting strategies for future campaigns.

{ “@context”: “https://schema.org”, “@type”: “FAQPage”, “mainEntity”: [ { “@type”: “Question”, “name”: “Should you pre-generate coupon codes or create them on demand?”, “acceptedAnswer”: { “@type”: “Answer”, “text”: “Pre-generation is preferred for large campaigns. A batch job generates cryptographically random codes (e.g., 12-character alphanumeric), hashes or stores them in a database, and loads unissued codes into a Redis set or a queue. At distribution time the service atomically pops a code (SPOP in Redis), assigns it to a user, and marks it as issued. On-demand generation works for small volumes or personalized codes, but under high traffic it risks code collisions and adds latency from uniqueness checks. Pre-generation front-loads the work and makes distribution O(1).” } }, { “@type”: “Question”, “name”: “How do you prevent a coupon from being redeemed more than once?”, “acceptedAnswer”: { “@type”: “Answer”, “text”: “Use an atomic Redis SET NX (set-if-not-exists) on the coupon code key as a distributed lock before writing the redemption to the database. The flow is: (1) SET coupon::redeemed 1 NX EX 86400 — if this returns OK, the current request owns the redemption; (2) validate business rules (expiry, user eligibility) and write the redemption record to the DB in a transaction; (3) on any failure, delete the Redis key to release the lock. If the SET NX returns nil, the code is already redeemed and the request is rejected immediately." } }, { "@type": "Question", "name": "How do you detect coupon fraud such as mass redemption attempts?", "acceptedAnswer": { "@type": "Answer", "text": "Implement multi-layer rate limiting: (1) per-user limit enforced via a Redis sliding window counter keyed on user ID — e.g., no more than 3 redemptions per hour; (2) per-IP limit to catch credential-stuffed accounts sharing an IP; (3) velocity checks that alert when a single code is attempted by more than N distinct users in a short window, indicating code leakage; (4) device fingerprinting to group users who rotate IPs. Flagged events are written to an audit log and can trigger automatic code invalidation or account review." } }, { "@type": "Question", "name": "What coupon types should the system support and how are they modeled?", "acceptedAnswer": { "@type": "Answer", "text": "Store a discount_type enum and a discount_value on each coupon record. Common types: PERCENT_OFF (e.g., 20% off subtotal), FIXED_AMOUNT (e.g., $10 off), FREE_SHIPPING (zeroes shipping line), BOGO (buy-one-get-one — requires item quantity logic in the cart service), and MINIMUM_ORDER (applies only when cart exceeds a threshold). The cart service reads the coupon record, switches on discount_type, and applies the corresponding calculation. Keeping the logic in the cart service — not the coupon service — respects service boundaries and makes it easy to add new types without changing the coupon storage schema." } }, { "@type": "Question", "name": "How do you enforce coupon expiry reliably?", "acceptedAnswer": { "@type": "Answer", "text": "Store expires_at as a UTC timestamp on the coupon record and enforce it in two places: (1) at redemption time the API checks expires_at before accepting the code — this is the authoritative check; (2) a nightly batch job marks expired codes as EXPIRED in the database so queries and dashboards reflect the correct state without needing real-time scans. For campaigns with strict end times, also set a Redis TTL on the active campaign key so that even a caching layer stops serving the code after expiry without waiting for the next cache invalidation cycle." } } ] }

Frequently Asked Questions

Q: Should you pre-generate coupon codes or create them on demand?

A: Pre-generation is preferred for large campaigns. A batch job generates cryptographically random codes (e.g., 12-character alphanumeric), hashes or stores them in a database, and loads unissued codes into a Redis set or a queue. At distribution time the service atomically pops a code (SPOP in Redis), assigns it to a user, and marks it as issued. On-demand generation works for small volumes or personalized codes, but under high traffic it risks code collisions and adds latency from uniqueness checks. Pre-generation front-loads the work and makes distribution O(1).

Q: How do you prevent a coupon from being redeemed more than once?

A: Use an atomic Redis SET NX (set-if-not-exists) on the coupon code key as a distributed lock before writing the redemption to the database. The flow is: (1) SET coupon:<code>:redeemed 1 NX EX 86400 — if this returns OK, the current request owns the redemption; (2) validate business rules (expiry, user eligibility) and write the redemption record to the DB in a transaction; (3) on any failure, delete the Redis key to release the lock. If the SET NX returns nil, the code is already redeemed and the request is rejected immediately.

Q: How do you detect coupon fraud such as mass redemption attempts?

A: Implement multi-layer rate limiting: (1) per-user limit enforced via a Redis sliding window counter keyed on user ID — e.g., no more than 3 redemptions per hour; (2) per-IP limit to catch credential-stuffed accounts sharing an IP; (3) velocity checks that alert when a single code is attempted by more than N distinct users in a short window, indicating code leakage; (4) device fingerprinting to group users who rotate IPs. Flagged events are written to an audit log and can trigger automatic code invalidation or account review.

Q: What coupon types should the system support and how are they modeled?

A: Store a discount_type enum and a discount_value on each coupon record. Common types: PERCENT_OFF (e.g., 20% off subtotal), FIXED_AMOUNT (e.g., $10 off), FREE_SHIPPING (zeroes shipping line), BOGO (buy-one-get-one — requires item quantity logic in the cart service), and MINIMUM_ORDER (applies only when cart exceeds a threshold). The cart service reads the coupon record, switches on discount_type, and applies the corresponding calculation. Keeping the logic in the cart service — not the coupon service — respects service boundaries and makes it easy to add new types without changing the coupon storage schema.

Q: How do you enforce coupon expiry reliably?

A: Store expires_at as a UTC timestamp on the coupon record and enforce it in two places: (1) at redemption time the API checks expires_at before accepting the code — this is the authoritative check; (2) a nightly batch job marks expired codes as EXPIRED in the database so queries and dashboards reflect the correct state without needing real-time scans. For campaigns with strict end times, also set a Redis TTL on the active campaign key so that even a caching layer stops serving the code after expiry without waiting for the next cache invalidation cycle.

See also: Shopify Interview Guide

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

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

Scroll to Top