[{"data":1,"prerenderedAt":17},["ShallowReactive",2],{"api:\u002Fapi\u002Fconfig:en":3,"api:\u002Fapi\u002Fnotes\u002Fdouble-booking:en":8},{"email":4,"lang":5,"name":6,"role":7},"hello@ewsirdev.example","en","Ewsirdev","Web developer: websites, landing pages and Go services",{"slug":9,"lang":5,"title":10,"date":11,"summary":12,"project":13,"minutes":14,"body_md":15,"body_html":16},"double-booking","Double booking: a transaction plus a partial UNIQUE index","2026-10-20","How booking-slots protects against two clients booking the same time at once, and how that protection is tested.","booking-slots",1,"The classic race: two clients see a free slot at the same moment and both press \"book\". A single \"is it free\" check in code is not enough: a second request can slip in between the check and the insert.\n\n## First line: a transaction\n\nIn `Book`, recalculating the free slots and inserting the booking happen in one transaction. A slot that is not among the free ones yields `ErrTaken` (or `ErrPast` when the time has already passed).\n\n## Second line: an index\n\nThe database itself refuses to create a duplicate:\n\n```sql\nCREATE UNIQUE INDEX IF NOT EXISTS ux_bookings_active_start\n  ON bookings(date, start_min) WHERE status = 'active';\n```\n\nThe index is **partial**: it applies only to active bookings, so a cancelled booking frees the time. If the insert hits the index, the code turns the error into the same `ErrTaken` and the client gets 409.\n\n## How it is tested\n\nA test launches 30 goroutines for the same time: exactly one booking must succeed.\n\n## Boundaries\n\nThis protects a single resource: all services share one schedule, and a multi-specialist model would need a different index. The project's limitations say so.\n","\u003Cp>The classic race: two clients see a free slot at the same moment and both press &#34;book&#34;. A single &#34;is it free&#34; check in code is not enough: a second request can slip in between the check and the insert.\u003C\u002Fp>\n\u003Ch2>First line: a transaction\u003C\u002Fh2>\n\u003Cp>In \u003Ccode>Book\u003C\u002Fcode>, recalculating the free slots and inserting the booking happen in one transaction. A slot that is not among the free ones yields \u003Ccode>ErrTaken\u003C\u002Fcode> (or \u003Ccode>ErrPast\u003C\u002Fcode> when the time has already passed).\u003C\u002Fp>\n\u003Ch2>Second line: an index\u003C\u002Fh2>\n\u003Cp>The database itself refuses to create a duplicate:\u003C\u002Fp>\n\u003Cpre tabindex=\"0\">\u003Ccode class=\"lang-sql\">CREATE UNIQUE INDEX IF NOT EXISTS ux_bookings_active_start\n  ON bookings(date, start_min) WHERE status = &#39;active&#39;;\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>The index is \u003Cstrong>partial\u003C\u002Fstrong>: it applies only to active bookings, so a cancelled booking frees the time. If the insert hits the index, the code turns the error into the same \u003Ccode>ErrTaken\u003C\u002Fcode> and the client gets 409.\u003C\u002Fp>\n\u003Ch2>How it is tested\u003C\u002Fh2>\n\u003Cp>A test launches 30 goroutines for the same time: exactly one booking must succeed.\u003C\u002Fp>\n\u003Ch2>Boundaries\u003C\u002Fh2>\n\u003Cp>This protects a single resource: all services share one schedule, and a multi-specialist model would need a different index. The project&#39;s limitations say so.\u003C\u002Fp>\n",1792697892653]