A merchant who ships forty parcels a day doesn't want to fill in a form forty times. Most of them already keep those orders somewhere else, in a sp...
For further actions, you may consider blocking this person and/or reporting abuse
The rule I would add from my own batch work is treating the batch response itself as durable state: when a network drop hits after half the batch is created, the client replays the whole batch and must reconcile per-order results instead of assuming the batch failed. Your 409 on a changed payload is the part that saves you there, because a silent return of the old order would look like success while the merchant's correction never landed. Do you store the per-order result alongside the key, so a replay returns the same created and failed split?
Umm, yeah. In this design, each order gets its own key, so retries are safe per order, not just per batch. For created orders, the result is stored with the key, so a replay returns the same “created” instead of making a new one. Failed orders are not stored on purpose, so a replay checks them again and they go through once the merchant fixes them.
That per-order split is the part I would keep, because a batch-level key hides which line actually landed. I would still put a boundary on the failed-order replay: in my own batch work I saw a replay keep rechecking a line the merchant had not actually fixed, so it looked busy while nothing changed. Do you expire the created keys, or keep them until the merchant confirms the order?
Fair point, that would look like it’s working when nothing changed. In this design the sheet only resends a failed order if the merchant actually edited it. If nothing changed, it stays red with the same error, so there’s no fake “busy” loop. For created keys, they expire after about a day. That covers retries for the same sheet, and after that the merchant can reuse the order reference without a clash.
Keeping the failed order red unless the merchant edited it is the clean part, and a day for created keys is a tidy retry window.
The "only a created order claims the key" rule has one more edge: two requests with the same key in flight at once. A double-clicked confirm, or a client retry fired while the first request is still running. Both can read "no key yet" before either commits. A pattern that holds up is letting Postgres arbitrate with
UNIQUE (merchant_id, idempotency_key)andINSERT ... ON CONFLICT DO NOTHING RETURNING id. The second insert waits on the first transaction, gets no row back, and you then select the existing order and compare the payload hash. Worth one test that fires the same batch twice in parallel.Ha, a double click could easily do that. I think, letting the db decide with a unique (merchant_id, idempotency_key) is much cleaner than an “in progress” flag. The second request just waits, then finds the order that’s already there. Will do that parallel test, thanks!
Making the sheet match the file the merchant already has, instead of the shape that looked tidy in the design, is the decision everything else hangs on — every import tool that invents its own layout is really asking the user to do the translation work the software was supposed to do. Treating a repeated order reference on non-adjacent rows as an error rather than a silent merge is also exactly right: a merge guesses at intent, an error asks, and with orders the cost of a wrong guess is a parcel. The validate-everything-before-creating-anything rule is what makes the no-duplicates promise believable, because it turns a mid-batch failure into a clean retry instead of an archaeological dig through which twenty rows already became real orders.
Appreciate this! And agreed, guessing with orders gets expensive fast.
Using the merchant's own order reference as the idempotency key is neat, and it raises two edge cases worth a test each.
First, if a merchant restarts numbering, say each day's sheet begins at A-101 again, a new order gets last week's order back instead of being created. Scoping the key by merchant and storing a hash of the payload catches that: same key with a different payload should be a clear conflict, not a silent return of the old order.
Second, only a created order should claim the key. If a failed attempt stores it, the fixed resubmit you describe gets the old failure back.
Thanks, really good points! The restarted numbering one is sneaky, I hadn’t thought of that. And yeah, only a created order should claim the key, otherwise the resubmit flow breaks. Updated d post with both, appreciate it!