Skip to content

Build1 publisher2 min readPublished

One SQLite transaction keeps a retried Buy click from creating a second order

Retried Buy clicks get the original order back in a dev.to guide that commits each order with its attempt key in one SQLite transaction. A disabled button only cuts double-clicks, and the guide's available text ends before it covers charging the card.

The Engineer · Build desk

Illustration accompanying One SQLite transaction keeps a retried Buy click from creating a second order

What happened

  • A dev.to guide on checkout retries says disabling the Buy button or aborting the browser request cannot establish that the server stopped working.
  • In its design the server issues a quote ID for the offer, and the browser generates one idempotency key per logical purchase attempt.
  • A retry with the same customer, key and quote gets the stored result back, while the same key sent with a different quote is rejected.
  • The guide requires the order and its stored receipt to commit together in one transaction, or neither commits.

Compiled by The EngineerSomething wrong?How this is made

Why it matters

  • constraint Client code holds part of the guarantee. A fetch helper that mints a new key on every call gives each retry a new identity, and the server cannot tell it from a fresh purchase.
  • cost Every purchase endpoint needs a durable receipt store that compares each retry with the original request. With quote_id as the only field the comparison is easy; each field added to the request makes it harder.
  • decision Teams adapting the pattern for repeat purchases have to settle what counts as the same purchase before they can drop the one-quote, one-order constraint.

The guide starts from code that looks reasonable [12]:

``` if not receipt_exists(customer, key): order = create_order(quote) save_receipt(customer, key, order) ```

Two requests can both run the check before either one saves anything. Both see "not found," and both create an order [12]. The second failure needs no concurrency at all. The process can stop after it creates the order and before it saves the receipt [13]. When the retry arrives, the key has no receipt and the order already exists [13].

The runnable example handles both cases in one transaction on Python's standard-library SQLite [14]. The author picked it to keep installation simple while still using a real database instead of an in-memory dictionary [14]. A dictionary is a fine idempotency store until the process restarts. The transaction opens with BEGIN IMMEDIATE and looks up this customer's stored attempt before it creates anything. A match is either rejected as a different request or answered with the stored result [14].

The request leaves the browser very little to decide. The illustrative call is a POST to /purchases with an Idempotency-Key header and a body of {"quote_id":"quote_123"} [18]. The server takes the customer from authentication and the scope and price from its own quote record [7]. The guide adds that knowing a quote ID is not permission to use it, and points to OWASP's transaction authorization guidance for that boundary [7]. Quotes are immutable in the example, so a changed offer means a new quote [3].

If I were adopting this design, the unique constraint on the order's quote ID is the piece I'd keep even behind a framework that manages idempotency keys for me. A key identifies repeat deliveries of one attempt. The guide's point is that idempotency cannot know two different keys were meant to describe the same intent [10]. A request that arrives under a new key for an already-used quote is a new attempt as far as the receipt table knows. The constraint is what stops it becoming a second order [10].

Charging the card is the other half. The guide frames its subject as the boundary between creating an order and charging for it [15]. I think each side of that boundary needs its own key. The receipt records that an order exists. The charge is a separate write at a separate party, the payment provider. The guide says it will open up the payment-provider layer [15]. The available text ends partway through the SQLite transaction sequence, before that section begins [17].

What to watch

  • The guide's payment-provider section: whether the card charge carries its own idempotency key tied to the order, or reuses the purchase-attempt key.
  • How the example recovers a pending attempt after a page reload, which the guide says the application must do before checking the attempt's status.
Loading claim ledger
Loading source directory links
Loading share composer
Loading topic controls
Loading related stories