2
3 Comments

Two customers almost booked the same appointment slot in my SaaS. Here's the one-line bug that caused it

Two clients opened my booking platform, picked the same 2:00 PM slot at the same salon, and both hit "Confirm" within a couple hundred milliseconds of each other.

Both requests should have failed for one of them. Both succeeded.

Why this is scary and not just a bug: this is exactly the kind of failure a business owner doesn't notice until a customer shows up to a chair someone else is already sitting in. No error, no crash, no log line screaming at you — just a quietly overbooked afternoon.

What I tried first (and why it didn't work)

My first fix was the obvious one: check for a conflict, then insert the booking if there isn't one.

That works in every manual test I ever ran. It falls apart under real concurrency, because the check and the insert are two separate steps. Two requests can both check, both see nothing, and both proceed. Classic race condition — the fix looked right and was wrong.

The actual fix

I moved the conflict check into the database itself, as a trigger that runs in the same transaction as the write. Instead of "check, then maybe insert," it's "the database refuses the insert if a conflict exists — atomically, no gap for a second request to sneak through."

One extra wrinkle I didn't expect: bookings can have no assigned staff member yet (NULL), and in SQL, NULL = NULL isn't true — it's neither true nor false. A plain uniqueness check silently stops protecting anything the moment a field is optional. Postgres has a specific NULL-safe comparison operator for exactly this (IS NOT DISTINCT FROM), and that one detail was the actual fix, not the trigger itself.

What I took away from this

I'm not a formally trained backend engineer — I built this product mostly by directing Claude Code and thinking through the logic myself. The lesson that stuck with me: the bugs that scare me most now are the ones with no error message. A crash tells you something's wrong. A silent double-booking doesn't — you only find out when a real customer is standing in the wrong place at the wrong time.

Since then, my personal rule is: anything touching money, availability, or a shared resource gets a five-minute "what happens if this runs twice at once" pass before it ships. Not a formal audit — just one deliberate question I used to skip.

Pronto is the open-source booking/CRM/POS platform this happened in, if anyone wants to see the actual trigger: github.com/SGrappelli/pronto

Curious how other solo/non-technical founders building with AI coding tools catch this class of bug before it reaches production — do you have a checklist, or did you learn it the same way I did?

posted to Icon for group Show IH
Show IH
on July 29, 2026
  1. 1

    The IS NOT DISTINCT FROM detail is the real twist here. Most people would catch the race condition eventually, but the NULL comparison silently breaks after you fix it - that's the kind of thing that surfaces weeks later in production when someone first creates a booking without assigning staff. Great catch documenting that second layer. The "one deliberate question before shipping" rule for shared resources is solid - I'd extend it to anything where the cost of the bug compounds (bookings, payments, inventory reservations).

  2. 1

    the silent ones are always the worst: no crash, no log, just a furious customer two days later. classic read-then-write race; the fix almost always has to live in the database (unique constraint or a row lock on the slot), because app-level checks lose that millisecond gap every time. did a DB constraint end up being your one-liner, or did you solve it in code?

  3. 1

    This is a good example of why production bugs are often business problems, not just engineering problems.

    The interesting part isn't only fixing the race condition — it's building the habit of asking what happens when two real users do the same thing at the same time.

Trending on Indie Hackers
Stop losing deals in the gap between "sounds good" and getting paid User Avatar 64 comments We scanned 50,000 domains. Your cold email list is really four systems. User Avatar 55 comments Building a startup costs $0. Your tooling budget costs $500K. Here's why. User Avatar 50 comments 787 tools for developers. 5 for nurses. Two weeks of tracking 14,000 indie launches. User Avatar 38 comments Building in public: a chat assistant that runs your server so you don't have to live in the terminal User Avatar 24 comments 67K impressions in 2 days from a single Daily-Dev post — here's what happened User Avatar 24 comments