Skip to content
SIRCODESALOT
All notes

Note 03 · · 2 min read

The button was easy. Making it safe to press twice wasn't.

Checkout buttons, retries, and webhooks all happen more than once. Design for it on day one.

Every "Pay now" button gets pressed twice. Sometimes it's an impatient customer, sometimes a flaky phone connection retrying a request, sometimes Stripe redelivering a webhook because your server took eleven seconds to answer the first time. The button itself takes ten minutes to build. Making the second press harmless is the actual work.

When I rebuilt the Minute Marketing storefront, this was the part I spent the most time on, and almost none of it is visible.

Compute once, then freeze it

The first rule: the price a customer sees is calculated on the server, and the result is saved as a snapshot. Quantity, options, discounts, delivery, tax, all of it. The checkout then charges that snapshot, not a fresh recalculation.

That sounds like bookkeeping, but it closes a whole category of bugs. If a discount expires between the cart and the payment, or a product price changes mid-checkout, the customer still pays what they approved, and production builds exactly what they paid for. One source of truth, written once.

Every state change needs a key

Payments move through a small set of states, and each transition is keyed by something stable: the order, the payment intent, the webhook event ID. If the same event shows up twice, the second one finds the transition already recorded and does nothing.

The test I use: could I replay every request and event from yesterday, in a random order, and end up with the same database? If the answer is no, something is going to double-charge, double-ship, or double-email someone eventually.

Side effects go in an outbox

Emails and texts are the sneakiest duplicates. If you send the confirmation email inside the same request that marks the order paid, a retry sends it again. Worse, if the email provider is down, you either fail the payment request or lose the email.

So the order write and the "send this email" intent are stored together, and a separate worker drains the outbox. Retries are safe because the worker marks each message delivered, and an email outage just means the queue gets a little longer for a while.

The boring payoff

None of this shows up in a demo. Nobody screenshots an idempotency key. But it's the difference between a store that works on launch day and one that works on its hundredth busy Saturday, and it's a lot cheaper to design in than to bolt on after the first angry "why was I charged twice" email.

Dealing with something like this?

I'm happy to talk it through.

Get in touch