Booking system scaling
Booking system scaling and launch checklist
A booking system stays fast by paginating every list, indexing the main questions, and deferring heavy work until a measured trigger fires. At launch, apply 8 rules, watch 9 signals and hold the performance targets below.
Last checked
Rules to apply at launch
- Idempotency: Overlap prevention is a database exclusion constraint on (resource_id, during) for held and confirmed rows; booking is attempt-insert-and-catch-23P01, never check-then-insert; a concurrency test firing N parallel requests for one slot must yield exactly one success.
- Caching: Availability is computed on read for a bounded window (<= 31 days, one resource set) from rules minus time off minus booked ranges using the GiST index; results are cached at the edge for 15 s per (service, resource, window) and are advisory only; the exclusion constraint remains authoritative.
- Pagination: Booking lists, customer lists and history use keyset pagination on an indexed (sort_key, id) cursor, page size 25 capped at 100; calendar views read a bounded time range per resource, never the whole table.
- Rate limits: Token-bucket limits in Postgres per IP and per email on availability, booking creation, manage-link actions and reminder resends; per-customer cap on simultaneous held and future bookings per business; exceeded limits return 429 and hold no slot.
- Data growth: Holds are short (10 min) and bounded per customer and per IP so slot hoarding cannot starve inventory; expired holds are cleaned up lazily at insert and by cron, keeping the exclusion index small.
- Data growth: bookings and booking_events grow monotonically: partition by month on the booking start past the stated thresholds, anonymise old customers, and serve dashboards from utilisation_daily rollups instead of scanning bookings.
- Realtime: Staff calendars refresh via SSE fed by LISTEN/NOTIFY on the resource id or 15 s polling with ETag; customers see slot loss on the confirm step through the 409 response, not a live feed.
- Fanout: Business-wide events (holiday closure, staff leave) are one set-based insert into time_off for all affected resources plus a single job that cancels or reschedules impacted bookings in batches of 100.
Performance targets
| What to measure | Target |
|---|---|
| Availability API | p95 < 200 ms for a 14-day window per resource set, one GiST-backed range query plus rules expansion in memory |
| Booking creation (hold) | p99 < 100 ms, one insert, <= 5 queries |
| Booking page LCP | < 2.5 s on 4G mobile; shell edge cached, slots hydrated client-side |
| Calendar day/week view | p95 < 300 ms for 50 resources, one range read per week |
| Reminder timeliness | p99 reminder sent within 2 minutes of send_at |
| Double-booking rate | exactly 0 overlapping held or confirmed rows, verified by a nightly invariant query |
What to watch
- Metric booking conversion funnel: availability viewed -> hold created -> payment started -> confirmed, plus hold expiry rate.
- Metric slot conflict rate: share of booking attempts rejected by the exclusion constraint; spikes mean stale availability cache or hoarding.
- Metric reminder lag s and failure ratio: now minus send_at for the oldest pending reminder, send failures by channel.
- Metric no show and cancellation rate: per business and per service; drives deposit policy advice.
- Log booking event log: booking id, business id, transition, actor type; no customer names, emails, phones or notes.
- Trace availability to confirmation trace: one trace id across availability query, hold, payment and webhook confirmation.
- Slo booking availability: 99.9% of booking API calls return a definitive success or slot-taken response; 99% of reminders sent within 2 minutes.
- Alert overlap invariant violation: any overlapping active bookings on one resource pages immediately (should be impossible).
Recurring work
- send_reminders: runs cron 1m (claims reminders where send_at <= now()). Guard: SKIP LOCKED batch of 200, skips cancelled bookings, per-business daily cap, per-customer opt-out, attempt cap 3, idempotent by (booking_id, channel, send_at).
- expire_holds: runs cron 1m. Guard: sets held bookings past hold_expires_at to expired in bounded batches, status predicate makes it idempotent, cancels the unpaid payment intent.
- materialise_series: runs cron hourly + on series create/edit. Guard: extends each active series to the 90-day horizon, max 200 occurrences per run per series, conflicts recorded per occurrence not fatal, advisory lock per series.
- mark_no_shows: runs cron 5m. Guard: confirmed bookings ended more than no_show_grace_min ago without check-in become no_show; applies the deposit forfeit rule once; bounded batch.
- rollup_utilisation: runs cron hourly. Guard: recompute today and yesterday only, upsert by (resource_id, day).
- sync_calendars: runs queue (phase 2). Guard: per-connection backoff, token refresh failures disable the connection, API call budget per business per day.
Add when the numbers say so
- Read replicas: Serve public availability and calendar range reads from a read replica once primary CPU or availability p95 crosses the trigger; booking writes and holds always hit the primary.
- Storage: Shard the very largest businesses by business_id or move hot calendars to a dedicated tenant database once a single business exceeds the stated write rate.
Deferred until it is needed
| Deferred | Adopt it when |
|---|---|
| Two-way Google/Outlook calendar sync | paying businesses request it or > 20% of bookings are double-booked externally. |
| SMS provider integration at scale | no-show rate > 8% after email reminders. |
| Waitlist and auto-fill of cancelled slots | cancellations > 10% of bookings. |
| Read replicas for availability and calendar reads | primary CPU > 60% sustained or availability p95 > 300 ms. |
| Route-aware multi-location scheduling and dynamic pricing | mobile-service businesses or utilisation below 50% at peak demand. |
On Forge64
Forge64 matches your description to one of 12 reference architectures, writes the plan and data model into the project, generates the app, and runs a quality gate over the code before you see it. Each app gets its own Postgres database and sign-in with no per-user fee. Hosting on Forge Cloud and Forge Payments are rolling out.
Every metered resource on Forge64 has a hard cap, so a traffic spike stops at your limit instead of becoming a surprise bill. This is a reference design for the category, not a promise that every item is generated today. Built-ins such as email, background jobs, file uploads and search are coming soon, so anything here that needs them is something you add yourself for now.
FAQ
- When should I worry about scale for a booking system?
- Not before a measured trigger fires. For example, for "Two-way Google/Outlook calendar sync": paying businesses request it or > 20% of bookings are double-booked externally.
- What performance targets should a booking system aim for?
- Availability API: p95 < 200 ms for a 14-day window per resource set, one GiST-backed range query plus rules expansion in memory. Booking creation (hold): p99 < 100 ms, one insert, <= 5 queries. Booking page LCP: < 2.5 s on 4G mobile; shell edge cached, slots hydrated client-side.
- Does Forge64 cap my usage costs?
- Yes. Every metered resource has a hard cap, and paid plans add an overage spend cap that you set.
Build your app with Forge64
One prompt to a running app with auth, database, hosting and payments. Start free, no credit card required.
Start building for free