Idempotency keys, double-entry ledger, exactly-once payment processing, webhook delivery guarantees, and PCI DSS compliance architecture.
A payment processed twice destroys trust and may be impossible to fix without manual intervention. Every design decision at Stripe starts with: "what happens if this fails halfway?"
A payment processed twice destroys trust. Idempotency is non-negotiable โ every mutating request must carry a client-generated key that makes retries safe.
Every payment creates 2+ ledger entries (debit + credit). Sum of all entries always equals zero. Balance reconciles trivially โ no money can silently disappear.
Webhooks may fail โ retry with idempotency prevents double-notifying merchants. At-least-once delivery + idempotent handlers = exactly-once semantics.
Raw card numbers never touch Stripe's API servers โ tokenized at edge, stored in isolated vaults. Dramatically reduces compliance scope for the main system.
Network timeout โ client retries โ server processes both โ customer bank debited twice. Without idempotency this is not a hypothetical โ it happens constantly. A retry after a 504 is indistinguishable from a new request without an idempotency key tying them together.
Click "Next Step" to advance through a complete Stripe charge. Each step shows what happens on Stripe's infrastructure, including timing.
Enter an idempotency key and amount, then fire the charge multiple times. Watch how Stripe's cache prevents double-charging even under concurrent retries.
# Idempotency key implementation async def create_charge(amount, currency, customer_id, idempotency_key): # Check if we've seen this key before cached = await redis.get(f"idem:{idempotency_key}") if cached: return json.loads(cached) # Return exact same response as before # Acquire distributed lock for this key async with redis_lock(f"lock:{idempotency_key}", timeout=30): # Double-check after acquiring lock (race condition) cached = await redis.get(f"idem:{idempotency_key}") if cached: return json.loads(cached) # Process the charge result = await process_payment(amount, currency, customer_id) # Store result with 24h TTL await redis.setex( f"idem:{idempotency_key}", 86400, # 24 hours json.dumps(result) ) return result
Stripe's ledger is modeled on 700-year-old double-entry bookkeeping. Every payment creates at least two ledger entries. The sum of all entries is always zero โ any deviation indicates a bug.
| Account | Debit | Credit | Notes |
|---|---|---|---|
| Customer payment received | $100.00 | Gross amount customer paid | |
| Stripe processing fee | $3.20 | 2.9% + $0.30 = $3.20 | |
| Merchant net receivable | $96.80 | Net settled to merchant | |
| Stripe revenue | $3.20 | Stripe's fee income | |
| Balance check | $3.20 | $200.00 - $96.80 - $3.20 = 0 | Sum = $0 โ |
Double-entry means every debit has a matching credit. SUM of all ledger entries = 0 always. This is how Stripe knows no money has disappeared โ a simple SELECT SUM(amount) FROM ledger = 0 is a powerful consistency check. Any non-zero result indicates a bug in the payment processing code.
Merchants depend on webhooks for order fulfillment. Stripe retries for 72 hours to survive merchant outages, deployments, and transient failures.
# Webhook delivery with exponential backoff RETRY_DELAYS = [5, 30, 300, 1800, 7200, 18000, 36000, 86400] # seconds async def deliver_webhook(event_id: str, merchant_url: str, payload: dict): for attempt, delay in enumerate(RETRY_DELAYS): try: resp = await httpx.post( merchant_url, json=payload, headers={"Stripe-Signature": compute_signature(payload)}, timeout=30 ) if 200 <= resp.status_code < 300: await mark_delivered(event_id, attempt) return except Exception as e: logger.warning(f"Webhook attempt {attempt} failed: {e}") if attempt < len(RETRY_DELAYS) - 1: await asyncio.sleep(delay) await mark_failed(event_id) # Alert merchant via dashboard