DEV Community

Kadokey
Kadokey

Posted on

Idempotent Order Processing: The Key to Reliable Digital Delivery

Why idempotency matters more for digital goods

When you sell physical products, a duplicate order is an annoyance — you refund one shipment. When you sell digital goods like game keys or gift card codes, a duplicate order is a direct financial loss. The code is delivered instantly, redeemed in seconds, and cannot be "returned." If your system processes the same payment twice and delivers two codes for one purchase, you have lost real inventory.

This makes idempotent order processing not just a best practice but a survival requirement for digital storefronts. At Kadokey, where we deliver digital gift cards and game keys instantly, every order must be processed exactly once — no matter how many times the client retries.

The duplicate problem

Duplicates come from everywhere:

  • Client retries: The user's browser times out waiting for a response, so they click "Buy" again. Or their mobile app automatically retries the request.
  • Network issues: A load balancer retries a request that actually succeeded but whose response was lost.
  • Webhook duplicates: Payment providers send webhook events at least once — and "at least once" means "possibly twice." Your webhook handler must tolerate redelivery.
  • Double fulfillment: A background job crashes after delivering the code but before marking the order complete. On restart, it delivers again.

Without idempotency, each of these creates a second order, a second code delivery, and a support ticket.

Idempotency keys: the core mechanism

The standard solution is an idempotency key: a unique token the client generates for each logical operation (typically a UUID). The client sends it with the request, and the server uses it to detect duplicates.

The flow:

  1. Client generates idempotency_key = uuid4() for the checkout attempt.
  2. Client POSTs /orders with the key in a header or body field.
  3. Server checks: have I seen this key before?
    • No: Process the order, store the key with the result, return the result.
    • Yes: Return the stored result WITHOUT reprocessing.

The critical detail: the key must be stored BEFORE processing begins (or atomically with it), and the result must be cached. Otherwise, two concurrent requests with the same key can both pass the check.

Implementation patterns

Database unique constraint (simplest)

Create an idempotency_keys table with a UNIQUE constraint on the key. In the same transaction that creates the order, insert the key. If the insert fails with a unique violation, it is a duplicate — fetch and return the existing order.

CREATE TABLE idempotency_keys (
  key TEXT PRIMARY KEY,
  order_id UUID NOT NULL,
  created_at TIMESTAMPTZ DEFAULT NOW()
);
Enter fullscreen mode Exit fullscreen mode

This works because the database serializes the inserts. The loser of the race gets a constraint violation and knows to return the existing result.

State machine with conditional writes

For more complex flows, model the order as a state machine: pending → processing → completed (or failed). Use conditional updates (UPDATE ... WHERE status = 'pending') so only one worker can transition the order. This prevents double-fulfillment even if the idempotency key check is bypassed.

Response caching

Store the full API response keyed by idempotency key (in Redis or the database) with a TTL (e.g., 24 hours). On duplicate, return the cached response with the same status code. This ensures the client sees identical responses, which matters for clients that validate response bodies.

Edge cases that bite

  • Key reuse across different operations: If a client accidentally reuses a key for a DIFFERENT order (different items/amount), you must detect the mismatch and return an error — not the old order. Store a hash of the request parameters with the key and compare.
  • Expired keys: If you expire keys too aggressively, a legitimate retry after expiry creates a duplicate. Choose TTLs based on your client's retry behavior (24h is a safe default).
  • Non-idempotent side effects: Sending the "your code is ready" email should also be idempotent, or you spam the customer. Use the same key to deduplicate notifications.

Testing it

Idempotency is notoriously hard to test manually. Automate:

  1. Send the same request twice with the same key → assert one order, identical responses.
  2. Send two DIFFERENT requests with the same key → assert an error (not a second order).
  3. Simulate a crash between fulfillment and key storage → assert no duplicate on retry.
  4. Fire 10 concurrent requests with the same key → assert exactly one order.

The bottom line

For physical goods, idempotency is good hygiene. For digital delivery, it is the difference between profit and loss. Every duplicate code delivery is inventory you cannot recover. Build idempotency in from day one — retrofitting it onto a system that already double-delivers is far more painful.

If you are building a storefront for digital products, start with idempotency keys on order creation and webhook handlers. Your future self (and your support team) will thank you.

Top comments (1)

Collapse
 
suppdevbot profile image
DEV SUPPORTS •

You need to verify your account.

Enter fullscreen mode Exit fullscreen mode

tr.ee/dev-to