Art of Codeartofcode
LLeeaarrnniinngg
BBlloogg
TToooollss
GGaammeess AArreennaa
JJoobbss
LLiivvee CCoouurrsseess

Courses

  • System DesignHigh-level system design, one everyday scene at a time: 30 concepts and 15 interview rounds.712 lessons
All coursesTutorialsMy Learning

Categories

  • 01AI & Tools
  • 02Tutorials
  • 03Engineering
  • 04Reviews
  • 05Build Logs
  • 06Tales of Misfits
  • 07Opinion
  • 08Career
  • 09News

Read & write

All storiesEvery post, newest first.Write for usPublish your stories and earn from what you write.Writer leaderboardsWho is writing the most read posts.

Categories

  • PDF Tools25
  • Development09
  • Utilities10
  • Design04
  • Converters04
  • Generators02

Featured

JSON FormatterFormat, validate and minify JSON.Regex TesterTest patterns against live text.Merge PDFCombine PDFs in your browser.JWT DecoderRead a token’s header and claims.Password GeneratorStrong passwords, generated locally.Gradient GeneratorBuild CSS gradients visually.
All 53 tools

Play a round

DevdleOne developer word a day, six guesses.Python RushClassic snake, with coding icons to eat.Blink or BreakA quick test of your reflexes.
Games Arena

Learn with us

Live CoursesInstructor-led courses. Join a waitlist and be first in when one starts.LearningStructured programming courses with AI assistance and certificates.

System Design

24. APIs and Idempotency

Lesson 15 of 17

24. APIs and Idempotency

1
The picture to keep in your head
2
24.1 What is an API contract?
3
24.2 What are the main API styles?
4
24.3 How do you version an API?
5
24.4 How does pagination work (offset vs cursor)?
6
24.5 How should errors and status codes work?
7
24.6 How does API auth work, in one page?
8
24.7 What is the double-charge problem?
9
24.8 How do idempotency keys work?
10
24.9 How do you retry safely?
11
24.10 How do webhooks work?
12
Memory card and where the analogy breaks
13
Practice: five levels
14
Interview questions
15
Solutions
16
Sources
17
Code lab
24. APIs and Idempotency

Solutions

3 min read+10 XP

Solutions

Level 1

reveal

Level 2

reveal

Level 3

reveal

Level 4

reveal

Level 5 (sketch)

reveal

No tracks available

Lofi Player

artofcode

artofcode.in

The den of devs.

Stories, tools, games, and gear for developers.

Read the StoriesEExxpplloorree TToooollss
Art of Codeartofcode

Platform

  • BBlloogg
  • TToooollss
  • GGaammeess AArreennaa
  • CCoouurrsseess
  • LLeeaarrnniinngg
  • CCaammppuuss

Company

  • AAbboouutt UUss
  • JJoobbss && IInntteerrnnsshhiippss
  • CCoonnttaacctt

Legal

  • TTeerrmmss ooff SSeerrvviiccee
  • PPrriivvaaccyy PPoolliiccyy
  • RReeffuunndd PPoolliiccyy

Copyright © 2026 artofcode.in, All Rights Reserved.

IST·--:--

1Show the Level 1 solution
2**A.**
31. **400** (or 422 if your API uses it for validation).
42. **401.**
53. **403** (or **404**, to avoid revealing that order exists).
64. **409.**
75. **429**, with `Retry-After`.
86. **503** (or 502/504 depending on how it fails).
9
10**B.**
111. **Yes**: GET is safe.
122. **No**: it might charge twice.
133. **Yes**: the key makes it safe.
144. **Yes**: PUT is idempotent.
155. **No**: a business-rule failure; it will fail again.
1Show the Level 2 solution
21. 1,000 × 0.005 = **5** double charges a minute; × 1,440 minutes = **7,200 a day**.
32. Page 2,000 starts at offset (2,000 − 1) × 50 = **99,950** rows skipped.
43. 2,000,000 × 1 KB = **about 2 GB** held at any time (24 hours' worth), plus index overhead.
54. 0.5 + 1 + 2 + 4 + 8 = **15.5 seconds** at most (full jitter picks a random wait up to each cap).
65. 4 attempts per layer, 4 layers: 4 × 4 × 4 × 4 = **256** calls. Retry at one layer only.
1Show the Level 3 solution
2Tested solution (Python 3), the key parts:
3
4```python
5def charge_with_key(self, key, order_id, amount, now=0, ttl=86_400):
6    body_hash = hashlib.sha256(json.dumps([order_id, amount]).encode()).hexdigest()
7    rec = self.keys.get(key)
8    if rec and now - rec["at"] > ttl:
9        rec = None                                   # expired: treat as new
10    if rec:
11        if rec["hash"] != body_hash:
12            return 422, {"error": "idempotency key reused with different parameters"}
13        if rec["status"] == "in_progress":
14            return 409, {"error": "a request with this key is still in progress"}
15        return rec["code"], rec["response"]          # replay
16    self.keys[key] = {"hash": body_hash, "status": "in_progress", "at": now}   # claim first
17    code, resp = 201, self._do_charge(order_id, amount)
18    self.keys[key].update(status="done", code=code, response=resp)
19    return code, resp
20
21
22def cursor_page(rows, after_id, limit):
23    if after_id is None:
24        return rows[:limit]
25    return [r for r in rows if r < after_id][:limit]
26
27
28def should_retry(method, status, has_key):
29    if status not in {408, 429, 500, 502, 503, 504}:
30        return False
31    return method in {"GET", "PUT", "DELETE", "HEAD", "OPTIONS"} or has_key
32
33
34def verify(secret, timestamp, payload, signature, now, tolerance=300):
35    if abs(now - timestamp) > tolerance:
36        return False                                 # too old: possible replay
37    expected = hmac.new(secret, f"{timestamp}.{payload}".encode(), hashlib.sha256).hexdigest()
38    return hmac.compare_digest(expected, signature)
39```
40
41All checks pass: two charges without a key and one with it; 422 for a reused key with a different amount; 409 while in progress; a new charge after 24 hours; the v1 client breaks on v2's price; offset pagination repeats order 91 while cursors don't; API keys verify only from their hash; POST without a key isn't retried; webhooks reject forged and stale deliveries and apply a retried one once. The full file is in `code/ch24_apis.py`.
1Show the Level 4 solution
2**The race:**
31. Request A arrives with key `checkout-883`. `db.get` finds nothing. A calls the card network (2 seconds).
42. The app times out and retries. Request B arrives with the **same key**. A hasn't saved anything yet, so `db.get` finds nothing. B calls the card network too.
53. Both charges go through. Both save the key. The customer pays twice. The tested demo reproduces this.
6
7**The fix:** **claim the key atomically before doing the work**, and make concurrent requests wait:
8
9```python
10def charge(key, order_id, amount):
11    body = fingerprint(order_id, amount)
12    try:
13        db.insert("idempotency", {"key": key, "body": body, "status": "in_progress"})  # UNIQUE(key)
14    except UniqueViolation:
15        rec = db.get("idempotency", key)
16        if rec.body != body:
17            return 422, "key reused with different parameters"
18        if rec.status == "in_progress":
19            return 409, "still processing; retry shortly"
20        return rec.code, rec.response                       # replay
21    code, response = card_network.charge(order_id, amount, idempotency_key=key)
22    db.update("idempotency", key, status="done", code=code, response=response)
23    return code, response
24```
25
26Also pass the key to the card network, so even a crash between charging and saving can't produce a second charge downstream.
1Show the Level 5 (sketch) solution
21. **REST + JSON** for partners and the mobile app (`/v1/...`); gRPC between the bookstore's internal services.
3   - `GET /v1/orders?limit=20&cursor=...`: list the caller's orders.
4   - `GET /v1/orders/{id}`: one order (authorised: it must be the caller's, or the partner's).
5   - `POST /v1/orders` with `Idempotency-Key`: body `{items: [{book_id, qty}], address_id, payment_method_id}`; returns `201` with `{id, status, total_paise, created_at}`.
62. **Cursor pagination**, newest first, opaque cursor of `(created_at, id)`, maximum limit 100.
73. **Problem Details** errors with machine codes: 400/422 validation, 401, 403 (or 404 for others' orders), 404, 409 (`order_already_paid`, `out_of_stock`), 429 with `Retry-After`, 503.
84. **End to end:** the app creates the key once per checkout attempt and stores it locally; the bookstore claims the key atomically, creates the order and an outbox entry in one transaction, and calls the payment provider **with the same key** (or one derived from the order ID); a retry at any point replays the stored result instead of creating a second order or charge.
95. **Shipment notifications:** an `order.shipped` webhook to each partner's registered URL, signed with HMAC-SHA256 over timestamp + body, with event IDs; partners verify, reject stale timestamps and de-duplicate by event ID. Deliveries are retried with backoff for a set period, with delivery logs and a "replay event" button; partners can also poll `GET /v1/orders/{id}`.