Skip to content
Frontend system design ยท about 45 minutes

Booking widget with payment

Design a booking widget that a business, for example a clinic or a salon, puts on its website. The customer chooses a service, a date and a free time slot, enters contact details, and pays by card. Many customers look at the same day at the same time, and two people must never get the same slot. The widget runs inside other websites, is used mostly on phones, and must work with a keyboard and a screen reader.

Ask first

  • Is payment required to confirm a booking, or can a booking exist without payment?
  • How long may a customer hold a slot while filling in the form and paying?
  • Do services have different lengths, and does the customer choose a staff member?
  • Which time zone do we show: the business's or the customer's?
  • How is the widget embedded: as an iframe, or as a script on the host page?
  • Can the customer cancel or move a booking in the widget, or is that out of scope?

The outline

Write your own outline for each part first, then open it to compare with the points a strong answer covers.

Requirements
  • Functional: choose a service, a date and a slot, enter details, pay, and see a confirmation with a reference
  • The server is the only authority on what is free; the client never decides that a slot is available
  • No double booking, also when two people press Pay in the same second
  • No double charge, also after a double click, a reload or a network loss during payment
  • When the slot is taken or the hold ends, the customer keeps the entered data and picks another slot
  • Card data never passes through our code or our server; the payment provider's fields collect it
  • Works on a phone, with a keyboard and with a screen reader
Architecture
  • A step flow: service, date and slot, details, payment, confirmation; a small state machine decides which step is allowed
  • Availability is server data, loaded per service and date, with a short cache time and a fresh load when the user returns to the step
  • Choosing a slot creates a hold on the server with an expiry time; other customers do not see the slot during the hold
  • Payment runs in the provider's hosted fields or page; our server creates the payment and confirms the booking when the provider reports success
  • The server prevents double booking with a transaction or a unique constraint; the client only reacts to a conflict response
  • Embedded as an iframe, which isolates styles and scripts from the host page; postMessage, with an origin check, reports the height and events to the host
Data model
  • Service: id, name, duration in minutes, price, currency
  • Slot: start time in UTC plus the time zone of the business, staffId if needed, and a status
  • Hold: id, slot and expiresAt from the server; the client shows a countdown, but the server decides
  • Booking draft on the client: serviceId, date, slot, holdId, customer details, step; kept in sessionStorage, never with card data, so a reload does not lose it
  • Booking: id, reference, status (pending payment, confirmed, cancelled, expired)
  • Flow state: the current step, a status for each request (idle, loading, error) and the last error
Interface
  • GET /services returns the list; GET /availability?serviceId=&date=2026-03-14 returns the free slots of that day
  • POST /holds { serviceId, slotStart } returns 201 { holdId, expiresAt }, or 409 Conflict when the slot was just taken
  • POST /bookings { holdId, customer } with an Idempotency-Key header returns the booking and what the provider's payment form needs
  • The provider tells our server the payment result with a webhook; the client asks GET /bookings/:id until the status is confirmed or failed
  • Embed: an iframe or script tag with the business id, the locale and the theme as parameters
  • Each step component gets the draft and an onNext(data) callback; steps do not call each other
Optimizations
  • The Pay button is disabled while the request runs, and a retry reuses the same idempotency key, so many clicks make one charge
  • The return from the payment page is not proof; the widget shows Confirmed only when our server says so
  • On 409 or an expired hold: explain it, keep the details, load availability again and go back to the slot step
  • The bank may ask for an extra confirmation (3-D Secure), so the flow has a clear 'payment pending' state
  • Time zones: store UTC, show the business's zone with its name, and test the days when clocks change
  • Accessibility: arrow keys in the date grid, slots as real buttons or radio inputs, errors linked to fields, focus moved to the heading of each new step
  • After a network loss during payment, read the booking status from the server before offering to pay again

Pitfalls

  • Checking availability on the client and then booking, with no hold and no check on the server
  • No idempotency key, so a retry or a double click charges the card twice
  • Showing success after the payment redirect, before the server confirmed the booking
  • Sending dates as local time text with no time zone
  • Losing everything the customer entered when the slot turns out to be taken

Follow-ups an interviewer asks

  • Two customers choose the same slot at the same moment. What does each one see?
  • The customer pays, and the network drops before the confirmation. What happens after a reload?
  • How long is a hold, and what does the customer see if it ends during payment?
  • Why an iframe, and what does it cost you?
  • The customer is in another time zone than the business. Which times do you show?
  • How would you test the double-booking case?