Idempotency Explained Without the Computer Science Lecture
A plain-English look at idempotency: the one question that tells you whether your software will double-charge, double-book or double-email a customer when something runs twice.

Here's the only question that matters: if this operation runs twice, does something happen twice? Not "should it". Does it, right now, in the code you've actually got.
That's idempotency. Forget the textbook definition about mathematical functions and repeated application. In practice it just means: running an operation more than once has the same effect as running it once. Click submit twice. The network drops and the app retries. A queue worker picks up the same job because it wasn't acknowledged in time. A cron job fires while yesterday's run is still finishing. All of these happen constantly in ordinary business software, not just at scale.
Why "running twice" happens more than you'd think
Nobody designs a system to run things twice on purpose. It happens because of things outside your control:
- A customer double-clicks "Pay Now" because the button didn't visibly disable.
- A payment gateway sends the same webhook twice because it never got a fast enough response the first time.
- A mobile app retries a failed request without knowing the server actually processed it before the connection dropped.
- A background job gets picked up by two workers because the queue's visibility timeout was too short.
- Someone reruns a script manually because it looked like it failed, when it had actually half-finished.
None of these are exotic. They're Tuesday. The question isn't whether your system will be asked to do something twice, it's what happens when it is.
Walking through the usual suspects
Payments
Run a card charge twice and the customer sees two transactions on their statement. This is the one people worry about most, and rightly so, but it's also the one most payment providers help you with directly. Stripe, for example, lets you attach an idempotency key to a charge request, so if the same key comes through again, it returns the original result instead of charging again. The mistake is not using that feature, or generating a new key on every retry instead of reusing the one from the original attempt.
Invoices
If your billing job runs twice for the same period, does the customer get invoiced twice? A common failure mode: a nightly job generates invoices for "any active subscription without an invoice this month", but the check for "without an invoice this month" runs before the first invoice is committed, so a slow run or an overlapping second run creates two. The fix is usually a unique constraint at the database level (one invoice per subscription per billing period), not just an application-level check.
Bookings
A customer's form submission times out on their end but actually succeeded on yours. They resubmit. Now you've got two bookings for the same slot, or worse, you've taken two deposits. Booking systems need to decide: is a duplicate booking for the same customer, same slot, same time window treated as one booking, or does it silently create a second? I built exactly this problem into how Patch handles deposits and recurring jobs, because tradespeople can't afford to turn up to a phantom appointment or chase a customer for a second deposit they never should have taken.
Emails and notifications
Less financially painful, but still damaging. A customer gets three "your order has shipped" emails because the notification job retried after a slow response, and now they've emailed you asking if something's gone wrong with their order. Trust erodes in small increments like this.
Stock movements
A warehouse scan gets submitted twice because the barcode reader double-fired, or a sync job runs against the same delivery note twice. Now your stock count is wrong, and nobody notices until a physical count doesn't match the system weeks later. Stock discrepancies caused by duplicate processing are some of the hardest bugs to trace back, because the symptom (wrong number) shows up long after the cause (duplicate event).
How to actually build for it
There are two broad approaches, and most real systems need both.
Make the operation naturally safe to repeat. Instead of "add 5 to stock", record "set stock movement ID 4471 as applied", where ID 4471 can only ever be applied once. Instead of "charge the card", check "has this order already been paid" before charging. This is usually the better fix because it doesn't rely on anyone remembering to pass the right key.
Use an idempotency key when the caller controls the retry. This is the pattern for anything triggered by an external system, an API call, a webhook, a form submission. The caller (or your frontend) generates a unique key for the attempt, and the server checks "have I seen this key before" and returns the previous result if so, rather than doing the work again. This is exactly how Stripe, most booking APIs, and well-built webhook receivers handle it.
Underpinning both is usually a database constraint, a unique index on something like (customer_id, slot_id) or (subscription_id, billing_period), that makes a duplicate physically impossible to insert, rather than just unlikely. Application-level checks help, but they race against themselves under load. Constraints don't.
A five-minute test you can run yourself
Pick a form in your app that does something meaningful, taking a payment, creating a booking, sending an invoice. Submit it, then immediately submit it again with the same details before the first one has visibly finished. Then check what actually happened in the database, not just what the screen showed. If you see two records where there should be one, you've found a live idempotency gap, and it's worth finding it yourself before a customer does.
This is also one of the first things I check when I'm asked to take an AI-built prototype and get it ready for real customers and real payments. Tools like Lovable, Bolt and Replit are good at building the happy path fast, but idempotency is exactly the kind of edge case that doesn't show up until someone's card gets charged twice in production. If that's where you are, I cover it in more depth on my AI prototype to production page.

