Being charged for an order once is fine; hitting 'submit' again after the page freezes and being charged a second time for the same order is not. A cashier who recognizes the same order number and hands you back the same receipt instead of ringing it up again is being idempotent — the operation can be retried without repeating what it did the first time.
A queued job I built at Infracommerce is exactly the kind of code that needs this discipline once it's off the request/response cycle: converting a synchronous customer-registration call to an external loyalty API into a RabbitMQ-queued job means that job can be retried — by a worker restart, a failed acknowledgment, a cron re-run — and a retried job that isn't idempotent double-syncs the same customer. Keying that job off the customer's own ID rather than 'did this job already run' so a retry lands on the same state instead of a duplicate one is the same principle a retried API request is supposed to honor with an idempotency key.
Trade-off: idempotency keys have to be generated by the caller and stored somewhere on the server for as long as a retry might plausibly happen — get the expiry window wrong and you either let a real retry double-charge, or reject a legitimate new request as a duplicate.
Before
class ChargeController
{
public function store(Request $request): JsonResponse
{
// a retried request charges the card again
$receipt = $this->gateway->charge($request->amount);
return response()->json($receipt);
}
}After
class ChargeController
{
public function store(Request $request): JsonResponse
{
$key = $request->header('Idempotency-Key');
if ($cached = Cache::get("charge:{$key}")) {
return response()->json($cached);
}
$receipt = $this->gateway->charge($request->amount);
Cache::put("charge:{$key}", $receipt, now()->addDay());
return response()->json($receipt);
}
}