
Stripe Payments Demo
Production-grade Stripe integration: webhook signature verification, Redis SETNX idempotency guard against duplicate event delivery, and payment intent creation with exponential-backoff retry. 29 tests covering exactly-once semantics, non-retryable 4xx errors, and concurrent duplicate suppression.
Performance & Impact
29 tests
Test Suite
idempotency (9) + webhook (8) + payments (5) + retry (6) + app (5)
SETNX
Idempotency
Exactly-once delivery — same Stripe event processed at most once in 24h
4 attempts
Retry Policy
Exponential backoff + jitter, non-retryable 4xx bypassed immediately
The Problem
Stripe delivers webhooks at-least-once — the same payment_intent.succeeded event can arrive 3–5 times during retries. Without an idempotency guard, each delivery triggers a fulfillment action (email, inventory update, DB write), causing duplicate orders and corrupted state.
The Solution
Redis SETNX on the event ID: the first delivery writes the key and processes. All subsequent deliveries hit the existing key and return 200 immediately without reprocessing. 24-hour TTL covers Stripe's full retry window. Payment intent creation uses a caller-supplied idempotency key, so double-clicks or network retries never create duplicate charges.
System Architecture
Core Engineering Achievements:
System Architecture
API Layer
Stripe Integration
Idempotency (Redis)
Retry Logic
"Express app with route-level middleware separation: raw body parsing only on /webhook (required for Stripe signature verification), JSON parsing everywhere else. Idempotency and retry are separate, independently testable modules — each with its own test file."
The Engineering Challenge
The non-obvious design decision was the retry policy for payment creation: retrying on 4xx (card decline, bad idempotency key) is harmful — Stripe will return the same error every time. The retry logic checks `statusCode >= 500` and treats all other errors as non-retryable. Network errors (no statusCode) are retried because they're transient. Jitter (0–25% of the exponential delay) prevents multiple failing payments from hitting Stripe simultaneously after a 5xx burst.
User Journey
Internal Design Doc
Webhook Idempotency, End to End
Stripe retries every webhook until it gets a 2xx. The only reason duplicate deliveries stay harmless is the eight lines of SETNX that sit between HMAC verification and the business handler. Here is what that looks like on the wire.
Sequence: first delivery vs. duplicate retry
/api/stripe/webhookTip: swipe sideways to pan the diagram.
Walkthrough
Redis state, before and after
Trace the keys a real duplicate delivery produces. The store starts empty, the first webhook writes one key with a 24-hour TTL, and every subsequent retry of the same event bounces off that key.
Before first delivery
t=0.000sredis> KEYS stripe:event:* (empty array) redis> GET stripe:event:evt_abc123 (nil)
The store has never seen this event id.
After first delivery
t=0.041sredis> SETNX stripe:event:evt_abc123 1 (integer) 1 redis> EXPIRE stripe:event:evt_abc123 86400 (integer) 1 redis> TTL stripe:event:evt_abc123 (integer) 86400
Key claimed. Handler runs, order fulfilled, 200 returned.
Duplicate retry arrives
same evt_abc123, new delivery attemptredis> SETNX stripe:event:evt_abc123 1 (integer) 0 // already exists — we bail redis> TTL stripe:event:evt_abc123 (integer) 86398 // same key, ~2s burned
Why the handler is skipped: SETNX returning 0 is our signal that this exact event id has been processed in the last 24 hours. We short-circuit, return 200, and Stripe stops retrying. No duplicate charge reconciliation, no double email, no phantom inventory decrement.
The 4xx retry trap
What breaks when you retry on every error
Retrying on transient failure is correct. Retrying on a Stripe 4xx is not — it turns a single declined card into four dashboard failures and four operator tickets.
Naive retry (wrong)
anti-patternfor (let i = 0; i < 4; i++) {
try {
return await stripe.paymentIntents
.create(params);
} catch (err) {
// Retry everything. What could go wrong?
await sleep(backoff(i));
}
}
throw new Error("out of retries");- •Card declined (402) triggers 4 useless retries.
- •4 duplicate failed attempts show up in the Stripe dashboard.
- •Issuer fraud scoring climbs. Good cards start getting blocked.
- •Missing idempotency key means each retry could double charge.
What we do
productionasync function withRetry(fn, { tries = 4 } = {}) {
for (let i = 0; i < tries; i++) {
try {
return await fn();
} catch (err) {
const code = err?.statusCode ?? 0;
// 4xx: Stripe said no. Stop.
if (code >= 400 && code < 500) throw err;
// 5xx or network: retry with backoff.
if (i === tries - 1) throw err;
await sleep(250 * 2 ** i);
}
}
}- •Network error: retry with exponential backoff.
- •5xx from Stripe: retry — their side, probably transient.
- •4xx from Stripe: fail fast, surface to caller.
- •Every call carries an
Idempotency-Keyheader so retries are safe.
The decision: trust Stripe's 4xx responses. They already thought about it.
Interested in the full engineering breakdown?
I'm always open to discussing technical implementations, from state management strategies to infrastructure scaling.