Idempotency
debt(d7/e5/b7/t5)
Closest to 'only careful code review or runtime testing' (d7). The detection_hints state automated detection is 'no' and the only tool listed is semgrep, which can catch specific code patterns (INSERT without ON DUPLICATE KEY, payment processing without idempotency key) but only if custom rules are written — it won't catch the general design failure. Missing idempotency in webhook handlers or queue consumers is typically discovered at runtime when duplicates appear in production, not before.
Closest to 'touches multiple files / significant refactor in one component' (e5). The quick_fix describes needing to design write operations to be safely repeatable using UPSERT, check-before-insert, or idempotency keys. This isn't a one-line swap — it requires changing the data model (adding idempotency key columns), updating API handlers, modifying database queries, and possibly reworking retry logic across a component. It doesn't typically reach full architectural rework, but it touches multiple layers within a service.
Closest to 'strong gravitational pull' (b7). The term applies_to web, api, and queue-worker contexts — essentially all write-path interactions in a distributed system. Every future feature involving payments, webhooks, or queue consumers must be designed with idempotency in mind. The common_mistakes list shows it affects payments, message queues, auto-increment IDs, and DELETE behavior — it shapes how every write operation is designed, making it a persistent and broad architectural constraint.
Closest to 'notable trap (a documented gotcha most devs eventually learn)' (t5). The misconception field explicitly states the trap: developers believe GET is automatically safe and POST is never idempotent, but application-level idempotency is a separate design concern independent of HTTP method semantics. This is a well-documented gotcha — most developers learn it after their first duplicate-charge or duplicate-record incident — but it contradicts a partially correct mental model (HTTP methods DO have idempotency semantics) making it a notable, not catastrophic, trap.
Also Known As
TL;DR
Explanation
Idempotency is critical in distributed systems where network failures cause uncertainty about whether a request succeeded. HTTP GET, PUT, and DELETE are idempotent by definition; POST is not. For non-idempotent operations (payment processing, email sending), implement idempotency keys: the client generates a unique key per operation and includes it in the request; the server stores the key and result, returning the cached result for duplicate requests. In PHP APIs, store idempotency keys in Redis or a database with a TTL and return 200 with the original response body for duplicates.
Common Misconception
Why It Matters
Common Mistakes
- Not using idempotency keys for payment APIs — network retry charges the customer twice.
- Non-idempotent webhook handlers — message queues deliver at-least-once; handlers must be idempotent.
- Using auto-increment IDs for operations that may be retried — each retry creates a new record.
- DELETE not being idempotent — deleting an already-deleted resource should return 404 or 200, not an error.
Code Examples
// Non-idempotent payment — retry = double charge:
function charge(int $userId, float $amount): void {
$this->stripe->charge($userId, $amount); // No idempotency key
$this->db->insert('charges', ['user_id' => $userId, 'amount' => $amount]);
}
// Network timeout → client retries → two charges
// Idempotent:
function charge(string $idempotencyKey, int $userId, float $amount): void {
if ($this->db->exists('charges', ['key' => $idempotencyKey])) return;
$this->stripe->charge($userId, $amount, $idempotencyKey);
}
// Idempotency key — client generates a UUID for each logical operation
class PaymentController {
public function charge(Request $req): JsonResponse {
$key = $req->header('Idempotency-Key') ?? throw new BadRequest();
$cached = $this->cache->get('idem:' . $key);
if ($cached !== null) {
return response()->json($cached, 200); // replay stored result
}
$result = $this->gateway->charge($req->amount);
$this->cache->set('idem:' . $key, $result, ttl: 86400);
return response()->json($result, 201);
}
}