Stack: DRF · Project: TicketFlow Status: Published Prerequisite: Lesson 13 — REST done right
Objectives
- Implement
Idempotency-Keyfor real: store the response, replay the same one, expire keys. - Design naturally idempotent endpoints (PUT, DELETE) and protect the ones that can't be (the payment POST).
- Evolve the API without breaking clients (backward compatibility and versioning).
1. The problem: the retry that charges twice
The client sends POST /reservations/{id}/payments/, the network times out, it retries — and the server already processed the first one: two payments. Neither your code nor the queue prevents it: the client retries legitimately (networks fail). The answer is the idempotency contract:
POST /api/reservations/{uuid}/payments/
Idempotency-Key: 7f3c9a2e-... ← generated by the CLIENT, unique per intentServer side:
- The key didn't exist → process it, store the response associated with the key (with its status code).
- The key exists with the same request body → return the stored response (without reprocessing).
- The key exists with a different body →
422 Unprocessable(misuse: the key belongs to one intent). - Old keys expire (24h TTL) — they are a cache of intents, not a forever log.
Storage: a idempotency_key(key PK, request_hash, response_status, response_body, created_at) table — transactional with the processing (Lesson 10): the key and the effect land in the same commit.
2. Idempotency by design (free, no table)
| Operation | Idempotent? | Why |
|---|---|---|
PUT /events/{uuid} (full replacement) | yes | the same PUT twice leaves the same state |
DELETE /reservations/{uuid} (cancel) | yes if repeating is a no-op | 00b's cancel() already returned False |
GET | yes | mutates nothing |
POST /reservations/ | NOT by design (creates a new resource) | the intent-key protects it |
POST /payments/ | NO (it charges) | the intent-key is MANDATORY |
The project's I3 ("paid exactly once") holds through two layers: idempotency_key UNIQUE on Payment (DB) + this lesson's mechanism (API). Defense in depth again.
3. Backward compatibility: what you can change without breaking
Safe (old clients keep working): adding fields to responses; adding endpoints; adding optional parameters with defaults; adding new values the old client ignores.
Breaking (requires a version): removing or renaming fields; changing types (string→int); changing an error code's semantics; making a parameter required; changing the pagination format.
Operational rule: clients don't upgrade when you want them to (installed mobile apps, integrators' scripts). Every breaking change = a new major version coexisting:
/api/v1/events/ (frozen, security bugfixes)
/api/v2/events/ (alive)Versioning in the path (not in a header: it is easier to share and debug). Announced deprecation: a Deprecation header + a documented cutoff date, and per-version usage metrics before shutting v1 down (Lesson 46).
4. Idempotency on the webhook (its twin)
The gateway retries its webhooks (at-least-once, Lesson 30): the same payment event may arrive 3 times. Your handler: verify the signature → look up payment.idempotency_key = the gateway's event_id → if it exists, answer 200 and do nothing (the successful retry is expected, not an error). This client/gateway symmetry is the reason I3 exists.
Self-assessment
- Why is the idempotency key generated by the CLIENT and not the server?
- What does the server store exactly to answer idempotently, and why is the request_hash mandatory?
- Why does the key table live in the same transaction as the effect (Lesson 10)? Which bug does separating them produce?
- List 3 "safe" and 3 "breaking" changes to your current API.
- How would you shut down v1 with data in hand and without breaking anyone abruptly?
Continue with the exercises. The solutions only after trying it yourself.