SDSystem design
Start learning free

LESSON 123 OF 140 · PRACTICE PROBLEMS

SYSTEM DESIGN · INTERVIEW WALKTHROUGH

Design a Coupon System: System Design Interview Practice

The problem

"Design a coupon system" for an online shop: a code like SPRING10 gives 10% off orders of $50 or more until Sunday, for the first 1,000 uses, once per customer. Some mailings send each customer a one-time code of their own, worth $20 once. Much of it is the Flash Sale again, so say so and spend the interview on the code itself.

The coupon service's calls, made by the shop's checkout:

POST /codes/SPRING10/check   {"cart_total": 60}
  → 200 OK         {"discount": 6}

POST /codes/SPRING10/use     {"order": "o81"}
  → 200 OK         {"use": "u5", "held_until": "12:10"}
  → 409 Conflict   {"result": "used_up"}

POST /uses/u5/release        (the payment failed)

The numbers

From the interviewer: someone posts SPRING10 on social media, and about 100,000 tries come in the first minute.

quantity         estimate
tries a second   100,000 ÷ 60 ≈ 1,700
tries per use    100,000 ÷ 1,000 = 100

Two easy slips: taking the minute's 100,000 as one second, or spreading them over a day (about 1 a second). 99 in 100 tries will hear "used up", and 1,700 a second is about a third of the 5,000 updates one row takes: that decides the rush.

Decision 1: the rules

When Ana applies SPRING10 to her $60 cart, the service checks every rule. Most read only the code's rules, kept on its row: the code exists and Sunday 23:59 hasn't passed, the cart is at least $50, and the discount is 10% of $60 = $6. Those don't change during the campaign, so any server can cache them. Only two checks need state: Ana's (code, customer) row, the unique key that makes "once per customer" the database's job, and the count of uses left.

record   fields
code     SPRING10, percent 10, min_order 50, ends Sun 23:59, limit 1000, left 1000
use      code, customer, order, status (held or used), held_until
         unique (code, customer)

Decision 2: check, then use

Checking the code in the cart changes nothing. Many shoppers apply a code and never pay: if applying took a use, SPRING10 would say "used up" while most of its uses sat in abandoned carts. Waiting until after the payment is wrong the other way: the money is taken before you know a use is left, so more than 1,000 shoppers can pay the discounted price at once, and the extras mean refunds or a loss.

So the use is taken when Ana places the order: one step takes one of the 1,000 and saves her use row as held, like the Flash Sale's holds. If the payment goes through, it's marked used. If it fails, or isn't finished in 10 minutes, the use is put back: the count goes up by 1 and her row is removed, so she can try again.

Decision 3: the rush

Every use of SPRING10 changes one number, the count of uses left: a hot counter. The take must be one atomic step that refuses below 0:

UPDATE codes SET left = left - 1 WHERE code = 'SPRING10' AND left > 0

If no row changed, the code is used up. Reading the count and then writing one less lets two orders both see 1 left: 1,001 uses. At about 1,700 tries a second one row keeps up, so the Flash Sale's allowances (100 uses in each of 10 servers' memory) aren't needed. Built for 100,000 taps a second, they'd still cost their price: a server saying "used up" while another holds uses. Once the count hits 0, a used up flag that every server caches answers the rest of the tries without the row.

Decision 4: one-time codes

For a mailing of 1 million codes worth $20 each, a code generator picks 12 random characters per customer from a secure random source, out of 32 that are hard to mix up (no 0 or O, no 1 or I), and saves them in batches: about 200 seconds at 5,000 rows a second.

32 characters, 12 times, give about a billion billion codes, so a made-up code hits one of the 1 million about 1 time in a trillion: random ids can't be guessed (the Unique IDs lesson). The unique key refuses the rare repeat. Six characters, easier to type, give only about 1 billion codes, so about 1 guess in 1,000 hits; numbered codes are worse, since the next one is the last plus 1.

Decision 5: guessing

A script signs in and tries thousands of made-up codes an hour. Limit failed tries per account, say 5 an hour, with a token bucket that only failures spend (the Rate Limiting lesson), plus a limit per IP address for scripts that make many accounts. A typo costs one try; the script gets 5 an hour, about 44,000 a year, which at 1 in a trillion finds nothing. Locking an account after one wrong code punishes typos; a limit per IP address alone blocks offices and phone networks that share one address.

Same pattern as the Flash Sale

  • A count that never goes below 0: taken in one atomic step, never read and then written.
  • One per customer: a unique (code, customer) key, like the sale's (sale, customer).
  • Holds: a use is held until paid, put back if the payment fails.
  • A flag for the rest: once the code is used up, the answer comes without the row.

Summary

  1. Scope: the Flash Sale's pattern, plus rules, one-time codes and guessing.
  2. Data: rules on the code's row; one count of uses; a (code, customer) row per use.
  3. API: check in the cart, changing nothing; use at the order.
  4. Busy path: the order takes a use, held until paid, put back if the payment fails.
  5. Numbers: about 1,700 tries a second when a code goes viral.
  6. Scale: one atomic step on the count: take 1 only if any are left.
  7. One-time codes: 12 random characters, about 1 in a trillion to guess.
  8. Guessing: failed tries limited per account and per IP address.
Keep going with system designInteractive lessons from the building blocks to Twitter, Uber and payments, the way interviews ask. Free to start.
Start learning free