# Low Level Design: Coupon Distribution Service

Source: https://www.techinterview.org/post/3233472377/lld-coupon-distribution/
Updated: 2026-06-30 · techinterview.org

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:

- Pop one code from the Redis pool for the campaign

- Write a `user_coupon` row linking `user_id` to the code (unique constraint on `campaign_id, user_id`)

- Send the code via email or push notification with personalized messaging

- 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:

- **Validate code:** fetch from DB, confirm status is active

- **Check expiry:** reject if `expires_at` is in the past

- **Check usage cap:** reject if `uses_count >= max_uses`

- **Check minimum order:** reject if cart total is below `min_order`

- **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

- **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.

## 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](https://www.techinterview.org/post/3233459924/shopify-interview/)

See also: [Scale AI Interview Guide 2026: Data Infrastructure, RLHF Pipelines, and ML Engineering](https://www.techinterview.org/post/3233460275/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](https://www.techinterview.org/post/3233460268/stripe-interview-guide-2026-process-bug-bash-round-and-payment-systems/)
