[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"blog-prevent-double-booking-reservation-concurrency":3,"blog-nav-prevent-double-booking-reservation-concurrency":27},{"slug":4,"title":5,"description":6,"body":7,"category":8,"readTime":9,"tags":10,"coverImage":14,"coverImageAlt":15,"heroImage":16,"heroImageAlt":15,"metaTitle":17,"metaDescription":18,"ogImage":19,"noindex":20,"published":21,"publishAt":22,"sortOrder":23,"updatedAt":22,"status":24,"path":25,"bodyHtml":26},"prevent-double-booking-reservation-concurrency","How a reservation system prevents double booking","Two requests can both read a free slot. A unique or exclusion constraint, plus a hold that expires, is what rejects the second insert.","Two people can open the same free slot. Both requests read “available.” Both insert. The court, room, or person is now double-sold. The bug is the gap between the read and the write. It is not a careless receptionist, and it is not something the UI can fix by hiding the button.\n\n[Courtside Alley booking lessons](\u002Fblog\u002Fcourtside-alley-venue-booking-lessons-philippines), [booking and ops apps](\u002Fblog\u002Fbooking-ops-apps-mobile-web), and [domain modeling](\u002Fblog\u002Fwhy-we-model-the-domain-before-writing-controllers) all name this failure. None of them specify the lock. This article does. The venue story stays in those pieces.\n\n## The constraint is the lock\n\n`if (isAvailable()) { create(); }` loses under concurrency, even inside a transaction, unless that transaction holds a lock or the insert cannot violate a rule.\n\nWhen slots are fixed and cannot overlap by construction, a unique index on the resource and the start time is enough. When bookings have a duration, the rule is “these ranges must not overlap.” PostgreSQL [exclusion constraints](https:\u002F\u002Fwww.postgresql.org\u002Fdocs\u002Fcurrent\u002Fddl-constraints.html#DDL-CONSTRAINTS-EXCLUSION) express that. [PostgreSQL or MySQL](\u002Fblog\u002Fpostgresql-vs-mysql-for-applications) is why we do not pretend every engine has the same tool.\n\n`SELECT … FOR UPDATE` works when there is a row to lock, such as a pre-seeded slot. If “free” means the absence of a row, there is nothing to lock. The constraint has to reject the second insert.\n\n## Holds expire\n\nA slot someone is paying for is not free and not yet confirmed. Write a hold with an expiry, and let the same constraint cover holds and confirmed bookings. When the hold expires, release the row. Do not keep the hold in the browser and hope the second customer waits.\n\nThe payment webhook that confirms the hold has to be safe to receive twice. That is the idempotency problem in [one webhook, one side effect](\u002Fblog\u002Fidempotency-keys-webhooks-side-effects), applied to this row.\n\n## Straight next step\n\n[Tell us what is being reserved](\u002Fcontact) — a court, a room, a person, or a seat — and whether two bookings can overlap in time. We will name the constraint. We will not start from a calendar widget.","Engineering",7,[11,12,13],"Bookings","Database","Concurrency","\u002Fimages\u002Fjournal\u002Fprevent-double-booking-reservation-concurrency-cover.webp?v=20261013b","Open calendar planner with a coffee cup","\u002Fimages\u002Fjournal\u002Fprevent-double-booking-reservation-concurrency-hero.webp?v=20261013b","Preventing Double Bookings in a Reservation System | PrimeCode","Why two simultaneous bookings both see a free slot, and how a constraint and a short hold stop the duplicate.","\u002Fimages\u002Fjournal\u002Fprevent-double-booking-reservation-concurrency-og.webp?v=20261013b",false,true,"2026-10-21T01:00:00.000Z",54,"live","\u002Fblog\u002Fprevent-double-booking-reservation-concurrency","\u003Cp>Two people can open the same free slot. Both requests read “available.” Both insert. The court, room, or person is now double-sold. The bug is the gap between the read and the write. It is not a careless receptionist, and it is not something the UI can fix by hiding the button.\u003C\u002Fp>\n\u003Cp>\u003Ca href=\"\u002Fblog\u002Fcourtside-alley-venue-booking-lessons-philippines\" rel=\"noopener noreferrer\">Courtside Alley booking lessons\u003C\u002Fa>, \u003Ca href=\"\u002Fblog\u002Fbooking-ops-apps-mobile-web\" rel=\"noopener noreferrer\">booking and ops apps\u003C\u002Fa>, and \u003Ca href=\"\u002Fblog\u002Fwhy-we-model-the-domain-before-writing-controllers\" rel=\"noopener noreferrer\">domain modeling\u003C\u002Fa> all name this failure. None of them specify the lock. This article does. The venue story stays in those pieces.\u003C\u002Fp>\n\u003Ch2>The constraint is the lock\u003C\u002Fh2>\n\u003Cp>\u003Ccode>if (isAvailable()) { create(); }\u003C\u002Fcode> loses under concurrency, even inside a transaction, unless that transaction holds a lock or the insert cannot violate a rule.\u003C\u002Fp>\n\u003Cp>When slots are fixed and cannot overlap by construction, a unique index on the resource and the start time is enough. When bookings have a duration, the rule is “these ranges must not overlap.” PostgreSQL \u003Ca href=\"https:\u002F\u002Fwww.postgresql.org\u002Fdocs\u002Fcurrent\u002Fddl-constraints.html#DDL-CONSTRAINTS-EXCLUSION\" rel=\"noopener noreferrer\">exclusion constraints\u003C\u002Fa> express that. \u003Ca href=\"\u002Fblog\u002Fpostgresql-vs-mysql-for-applications\" rel=\"noopener noreferrer\">PostgreSQL or MySQL\u003C\u002Fa> is why we do not pretend every engine has the same tool.\u003C\u002Fp>\n\u003Cp>\u003Ccode>SELECT … FOR UPDATE\u003C\u002Fcode> works when there is a row to lock, such as a pre-seeded slot. If “free” means the absence of a row, there is nothing to lock. The constraint has to reject the second insert.\u003C\u002Fp>\n\u003Ch2>Holds expire\u003C\u002Fh2>\n\u003Cp>A slot someone is paying for is not free and not yet confirmed. Write a hold with an expiry, and let the same constraint cover holds and confirmed bookings. When the hold expires, release the row. Do not keep the hold in the browser and hope the second customer waits.\u003C\u002Fp>\n\u003Cp>The payment webhook that confirms the hold has to be safe to receive twice. That is the idempotency problem in \u003Ca href=\"\u002Fblog\u002Fidempotency-keys-webhooks-side-effects\" rel=\"noopener noreferrer\">one webhook, one side effect\u003C\u002Fa>, applied to this row.\u003C\u002Fp>\n\u003Ch2>Straight next step\u003C\u002Fh2>\n\u003Cp>\u003Ca href=\"\u002Fcontact\" rel=\"noopener noreferrer\">Tell us what is being reserved\u003C\u002Fa> — a court, a room, a person, or a seat — and whether two bookings can overlap in time. We will name the constraint. We will not start from a calendar widget.\u003C\u002Fp>\n",{"prev":28,"related":46},{"slug":29,"title":30,"description":31,"category":8,"readTime":32,"tags":33,"coverImage":37,"coverImageAlt":38,"heroImage":39,"heroImageAlt":38,"metaTitle":40,"metaDescription":41,"ogImage":42,"noindex":20,"published":21,"publishAt":43,"sortOrder":44,"updatedAt":43,"status":24,"path":45},"idempotency-keys-webhooks-side-effects","Idempotency keys: one webhook, one side effect","Retries are normal. Store the key with the result and enforce it with a unique constraint so a second delivery cannot create a second row.",6,[34,35,36],"API","Webhooks","Laravel","\u002Fimages\u002Fjournal\u002Fidempotency-keys-webhooks-side-effects-cover.webp?v=20261013b","Person writing on a printed form","\u002Fimages\u002Fjournal\u002Fidempotency-keys-webhooks-side-effects-hero.webp?v=20261013b","Idempotency Keys for Webhooks | PrimeCode","How an API stores an idempotency key so a retried webhook updates one record instead of creating a second side effect.","\u002Fimages\u002Fjournal\u002Fidempotency-keys-webhooks-side-effects-og.webp?v=20261013b","2026-10-19T01:00:00.000Z",53,"\u002Fblog\u002Fidempotency-keys-webhooks-side-effects",[47,49,64],{"slug":29,"title":30,"description":31,"category":8,"readTime":32,"tags":48,"coverImage":37,"coverImageAlt":38,"heroImage":39,"heroImageAlt":38,"metaTitle":40,"metaDescription":41,"ogImage":42,"noindex":20,"published":21,"publishAt":43,"sortOrder":44,"updatedAt":43,"status":24,"path":45},[34,35,36],{"slug":50,"title":51,"description":52,"category":8,"readTime":32,"tags":53,"coverImage":55,"coverImageAlt":56,"heroImage":57,"heroImageAlt":56,"metaTitle":58,"metaDescription":59,"ogImage":60,"noindex":20,"published":21,"publishAt":61,"sortOrder":62,"updatedAt":61,"status":24,"path":63},"api-rate-limiting-before-traffic","Rate-limit the API before the expensive routes exist","Login, OTP, search, webhooks, and unbounded page size are the routes to limit first. OWASP API4:2023 is the control, not a new security product.",[34,54,36],"Security","\u002Fimages\u002Fjournal\u002Fapi-rate-limiting-before-traffic-cover.webp?v=20261013b","Padlock resting on a laptop keyboard","\u002Fimages\u002Fjournal\u002Fapi-rate-limiting-before-traffic-hero.webp?v=20261013b","Rate-Limit the API Before Traffic Grows | PrimeCode","Where a Laravel API needs rate limits first — login, OTP, search, and webhooks — and what OWASP API4:2023 actually asks for.","\u002Fimages\u002Fjournal\u002Fapi-rate-limiting-before-traffic-og.webp?v=20261013b","2026-10-16T01:00:00.000Z",52,"\u002Fblog\u002Fapi-rate-limiting-before-traffic",{"slug":65,"title":66,"description":67,"category":8,"readTime":32,"tags":68,"coverImage":71,"coverImageAlt":72,"heroImage":73,"heroImageAlt":72,"metaTitle":74,"metaDescription":75,"ogImage":76,"noindex":20,"published":21,"publishAt":77,"sortOrder":78,"updatedAt":77,"status":24,"path":79},"queues-work-outside-the-http-request","Queues: work that does not belong in the HTTP request","The request commits the booking or the order. Mail, PDFs, webhooks, and exports belong on a worker, and that worker must be safe to retry.",[36,69,70],"Queues","Architecture","\u002Fimages\u002Fjournal\u002Fqueues-work-outside-the-http-request-cover.webp?v=20261013b","Warehouse aisles of packed orders waiting to move","\u002Fimages\u002Fjournal\u002Fqueues-work-outside-the-http-request-hero.webp?v=20261013b","Queues: Work That Leaves the HTTP Request | PrimeCode","Which web-app tasks belong in a Laravel queue — mail, webhooks, exports — and why a timeout is the wrong place to notice.","\u002Fimages\u002Fjournal\u002Fqueues-work-outside-the-http-request-og.webp?v=20261013b","2026-10-14T01:00:00.000Z",51,"\u002Fblog\u002Fqueues-work-outside-the-http-request"]