
Why your store should never sell the same product twice (and how we prevent it)
Double-selling is not bad luck: it is a design mistake that gets paid for with refunds, apologies, and trust. Here is how we solved it at URG Technology, where almost everything is a one-of-a-kind unit.
Picture the worst pair of messages a customer can receive. First: "thank you for your purchase." The next day: "sorry, that unit was already sold." In an online store, that is not bad luck, it is a design mistake, and it has a name: double-selling.
We ran straight into it while building URG Technology, a refurbished-computer store in Bogotá. There, almost everything is a one-of-a-kind unit: each machine has its own condition, its own price, and its own history. Two people wanting the same laptop at the same time stop being a rare scenario and become a daily risk.
What double-selling really costs
A double sale gets paid for three times. First the refund, with its paperwork. Then the apology, which eats up someone's time. And at the end, the most expensive part: the trust of a person who had already decided to buy from you and now has doubts. If your customer was buying something used, sight unseen, that doubt costs double.
There is a quiet cousin to the problem: phantom stock. These are customers who start paying, abandon the process, and leave the unit reserved forever. Nobody complains, nobody notices, and the store stops selling a product that is actually available.
Where it slips through
The first version of URG trusted whatever each customer's browser saw: the screen checked the stock, subtracted from it, and gave the go-ahead. That seems fine until two people show up at once. Both see "1 available," both pay, and the store lets both through. The root cause was that inventory was being deducted in the wrong place, with no ordering between requests.
There was a second design problem. That first version needed an administrator to have the panel open to process payment confirmations. If they closed their computer or went to sleep, the store stopped updating inventory. A store that depends on someone being awake is not an online store: it is a counter with business hours.
The atomic reservation, explained without jargon
Atomic, here, means "all or nothing": an operation that cannot be split in half. When someone tries to reserve a unit, the server locks that record for an instant, checks how many units are truly still free, and only then reserves it. If two attempts arrive together, one waits for the other: the second one finds the stock at zero and gets a "sold out" before it ever reaches payment.
On top of that sits a safety belt: a rule inside the database that makes it impossible for reservations to exceed what exists. Even if someone makes a coding mistake tomorrow, the database rejects the move. And every reservation records how it was made, when it was created, and when it expires.
Here is what the idea looks like in database terms. This is a simplified example meant to explain it, not the actual URG code:
BEGIN;
-- 1. Lock the row while deciding: the other attempt waits its turn.
SELECT stock, apartados
FROM productos
WHERE id = 'pc-123'
FOR UPDATE;
-- 2. Reserve only if a unit is truly still free.
UPDATE productos
SET apartados = apartados + 1
WHERE id = 'pc-123'
AND stock - apartados >= 1;
-- If none was left, no row gets updated: that is a "sold out".
COMMIT;
-- 3. The safety belt: never reserve more than what exists.
ALTER TABLE productos
ADD CONSTRAINT reservas_sin_exceso CHECK (apartados <= stock);Both pieces have been standard in any serious database for years. FOR UPDATE locks the rows you select, so another transaction trying to modify them has to wait until the first one finishes. [1] And a CHECK constraint forces a column's value to always satisfy a condition. [2] There is no magic here, just order.
Three clocks that work while nobody is watching
Reserving correctly is half the job. The other half is releasing what nobody actually used:
- Only a confirmed payment touches inventory. Opening the gateway reserves nothing. Once the payment is approved, the notice reaches the server, which validates its origin, checks the real status, deducts stock, and fires off the emails.
- A sweep every two minutes. A scheduled process checks expired reservations: it releases transfer payments that never got their proof uploaded in time, closes gateway attempts left open for more than an hour, and marks those orders as expired.
- Cross-cancellation. When a unit sells through one channel, the system goes through the pending reservations for that same product on the other channel and cancels them, in both directions.
Losing the race does not have to mean losing the sale
Here is the part almost nobody designs for. The customer who arrived second does not see a technical error: they see a message in red, "Someone was faster!", with a short explanation and a button that opens the catalog filtered to the category of the unit they lost. They also get an apology email with a 5% coupon.
The first version of that message said "Darn! You lost by a millisecond." We changed it because tone matters: that person did not lose a game, they lost a computer they wanted. The lesson cuts both ways. Integrity gets solved on the server, and even bad news can end in another sale.
The dangerous part is not failing: it is failing silently
An orphaned reservation does not raise a flag. A payment notice that never arrives does not either. That is why we built Centinela ("Sentinel") at URG, a health-monitoring module whose entire job is to be suspicious: it looks for expired reservations that never got released, payments left unconfirmed for over an hour, products with more units reserved than exist, and server functions with recent errors. Every alert arrives with its severity and a button to resolve it.
“A system that cannot tell you when it is broken will stay broken without anyone knowing.”
Five questions to know if your store is exposed
- 1Do you sell one-of-a-kind pieces or very limited units? Refurbished goods, spare parts, art, limited batches: there the risk is a daily one.
- 2Do you accept more than one payment method? Every new method is another door leading to the same product.
- 3Who deducts the stock? If the answer is "the page itself," you have the exact problem we had.
- 4What happens to a cart that starts checking out and then leaves? If nobody releases it, that product becomes invisible to everyone else.
- 5Does your store need someone to have the panel open? If the answer is yes, you are not really selling at night.
Selling the same product twice is one of those mistakes you only notice after it already happened. That is why you design against it beforehand. If you run a store with one-of-a-kind units, or you are about to build one, tell us what you sell and we will tell you how we would lock down your inventory.
Sources
References cited in the text with the [n] notation. Each link was verified and is publicly accessible.
- 1
Explicit Locking: bloqueos a nivel de fila con FOR UPDATE (Documentación oficial de PostgreSQL)
Primary source: FOR UPDATE locks the selected rows, and other transactions trying to modify them wait until the current transaction finishes.
https://www.postgresql.org/docs/current/explicit-locking.html - 2
Constraints: restricciones CHECK (Documentación oficial de PostgreSQL)
Primary source: a CHECK constraint forces a column's value to always satisfy a boolean expression on every write.
https://www.postgresql.org/docs/current/ddl-constraints.html
Know someone this helps? Share it.



