Before you start
- ✓A staging copy of the database you can put under heavy load
- ✓Query logging or tracing switched on for the busiest pages
- ✓A list of every action that touches money, stock or bookings
- ✓A way to make two requests at exactly the same time (a test tool, or two people)
1. "Check, then act": lock what you're about to change
The most common pattern in AI-generated code is to check that a slot is free, then book it. Two customers clicking together both pass the check and both get the slot. The fix is to repeat the check inside the database transaction, with the records locked in a consistent order. A check on the form still gives a friendly early message, but only the locked check is a guarantee.
2. Payment confirmations arriving twice: claim the work in one step
After a card payment, the provider's notification and the customer's returning browser often arrive together. If each creates the order when it finds none, both get through: on one of our platforms, that meant two orders for one payment. Instead, each tries to claim the payment in one database step and only the winner builds the order, backed by a rule allowing one order per payment. A claim abandoned by a crash can be retaken after a couple of minutes, and losing the race is treated as normal, not an error.
3. Deadlocks: always lock in the same order
Two baskets adding the same two items in opposite orders can block each other until the database gives up on one of them (a deadlock). When we removed a lock to cure one deadlock, it simply moved to other records: the answer was a consistent order, not more or fewer locks.
4. Lost updates: make "someone else changed this" work
When two people edit a record, the second save can silently overwrite the first. A version stamp catches this only if it travels with the form and the database is told which version the person started from. A common AI mistake passes it back in a way the code quietly ignores, so the protection never fires.
5. Too many queries: fetch once, pass the results down
A lookup per item in a list means a database call per item (the "N+1" problem), and helpers calling other helpers multiply it. One grouped query, with the results passed down, fixes it. On one page, about 60 database calls became about 28; for ten ticket types the estimate was around 150 down to about a dozen.
6. Batch jobs: don't lose your place as the list shrinks
A job processing "expired" records 100 at a time, a page at a time, skips work: each processed batch leaves the list, so the next page starts 100 records too far in. Ours stepped over 100 still-expired records per batch. Take the first 100 each time, or have parallel workers each claim their own records.
7. Prove each race with a test that fails every time
Races that only reproduce by luck don't get fixed. Our test tool pauses one simulated user at a chosen point in its database work, so two users can be interleaved exactly. Each known race became a test kept switched off until fixed; passing when switched on is what "fixed" means. It caught a plausible fix in an AI-drafted plan that would have left a basket reporting success while holding one ticket instead of four.
What usually goes wrong
From our own projects, not the manual.
Concurrency protection was set up but never used
Three kinds of record had a version stamp to catch clashing edits, but no form carried it and nothing handled the clash, so two people editing the same record silently overwrote each other. When you wire it up, the database has to be told which version the person started from: simply setting the value does nothing.
A payment retry reused the same reference
After a card was declined, the retry sent the same duplicate-prevention key, so the payment provider rejected every attempt and the customer couldn't pay. Use a new key only after a definite decline. Keep the same one after a timeout, or you risk charging twice.
A failed tracking write cancelled a paid order
A non-essential marketing record was saved in the same transaction as the order. When it failed, the whole order rolled back after the card had been charged. Keep side-effects out of the order's transaction, or make sure their failure is harmless.
The concurrency test passed while the bug was live
A test booking 7 and 5 tickets against 10 stayed within stock, so it passed, while two baskets were actually taking the same tickets. Test the thing you care about (who holds which ticket), not just the totals.
The first fix over-reported success
Our first fix trusted the error details after a clash and counted tickets as won that had actually been lost. After a clash, re-read the result from the database.
Do it yourself, or get help?
If your app has one of these patterns and your developer is comfortable with database transactions and locking, the fixes are well trodden. Not every app has every pattern, and the right fix depends on how it's built, so start with anything that touches money or stock. If you're seeing duplicate orders, overselling or deadlocks in production, or you're not sure where the races are, get help: these bugs rarely show up in testing and cost real money when they happen.
Get a developer on it