How to build a booking system: a step-by-step guide
To build a booking system you need five things working together: a service catalogue with real durations, availability stored as rules plus exceptions, a booking page that revalidates the slot when the client submits, a booking lifecycle (confirmed, rescheduled, cancelled, no-show, completed) with reminders, and a database constraint that makes double-booking impossible. Build them in that order: services, then availability, then the booking page, then the staff calendar, lifecycle, reminders and payments, reports last. If all you need is booking on an existing website, an embeddable scheduler does it in an afternoon. A custom online booking system for one business is a project of weeks, not days, and most of that time goes into the parts no screenshot shows.
This guide covers both paths. It is grounded in our hand-counted inventory of 92 live booking systems and in a build-off where seven booking apps were built from one planner by four coding models, all of which you can still log into.
Build, buy or customise?
| Your situation | Best path | Why |
|---|---|---|
| One person or one calendar, booking on an existing site | Embed a hosted scheduler | Availability, reminders and calendar sync are already solved |
| Several staff, rooms or locations, standard policies | Buy a vertical product (salon, clinic, studio) | Multi-resource availability is the expensive part, and they have built it |
| You need to own the data or host it yourself | Customise a self-hosted scheduler | You keep the engine, you take on updates, backups and email delivery |
| Booking is the product, or your rules don't fit anyone's settings | Build your own | Only worth it if the scheduling logic is what makes you different |
The last row is narrower than it looks. "Our cancellation policy is unusual" is a settings problem. "Every booking must reserve a technician, a van and a two-hour drive window" is a reason to build.
Building a website with a booking system: embed or build?
Most people searching how to build a website with a booking system already have a site and want clients to pick a time without an email exchange. For that, embed a scheduler. The well-known ones all support it (checked on 19 October 2026 on each vendor's own help pages):
- Calendly offers an inline embed, a pop-up text link and a floating pop-up widget, and its embed options overview lists embeds under all plans.
- Acuity Scheduling can be added as an embedded scheduler, a booking button or a booking bar; see adding Acuity Scheduling to your website.
- Cal.com supports an inline embed and a floating button that opens the booking form, documented in its embedding help pages.
An embed is the right answer when the business is one person or one calendar and the services are simple. Our free appointment scheduling software guide covers which free tiers hold up and the six tests to run before you share the link.
Build your own when the embed starts fighting you: staff who each perform different services at different prices, a room or chair that two staff share, deposits that depend on the service, a booking that has to land in your own customer records. At that point you are writing workarounds around someone else's data model, and the workarounds are the product.
What every booking system has to do
From our inventory of 92 live booking systems, counted from each product's public pages:
| Capability | Systems (of 92) |
|---|---|
| Availability engine: hours, buffers, booking windows | 89 |
| Payments, deposits and no-show policy | 79 |
| Confirmations, reminders and follow-ups | 77 |
| Intake forms and waivers | 77 |
| External calendar sync | 77 |
| Reports and exports | 66 |
| Public booking page or widget | 51 |
| Multi-location operations | 26 |
| Waitlist and gap-filling | 13 |
| Class schedules and capacity | 12 |
Read the top five as the core and the bottom three as branches you opt into. A counted row means the product advertises the capability publicly, not that it implements it well. Notice what sits at the top: availability, money and reminders. Not the calendar view.
First, decide what kind of booking you take
"Booking system" covers products that share almost no data model. A consultant's meeting link, a salon's staff-column calendar, a clinic's practitioner schedule, a fitness class with twelve places and a field-service dispatch board are all "booking". Pick one before you design a table:
- One-to-one appointments. A client books a service with a person (and sometimes a room).
- Classes. Many clients book one session with a capacity. Rosters, waitlists and check-in are load-bearing, and the one-to-one slot logic does not apply.
- Resources. Rooms, desks, equipment. Often internal, often no payment. Our meeting room booking system piece explains how little of a customer booking app that needs.
- Jobs at an address. Field service adds travel time and service areas, which is a different availability problem.
Write the answer down with an explicit list of what you are not building. Every later step depends on it.
The build order
Each step depends on the one before being real. The "done when" column is the check to run before moving on.
| # | Step | Done when |
|---|---|---|
| 1 | Booking type and scope | One sentence names the booking type, and a written list says what is out of scope |
| 2 | Service catalogue | Duration, buffer, price and eligible staff are editable per service, with nothing hard-coded |
| 3 | Staff, resources and availability rules | The slot generator returns the right slots for a test week containing a day off, an existing booking and a clock change |
| 4 | Public booking page | A client books on a phone without creating an account, and a slot taken in the meantime is refused at submit |
| 5 | Staff calendar and manual booking | Reception books from generated slot cards, and an overlapping booking is refused unless an authorised role overrides with a reason |
| 6 | Booking lifecycle | Reschedule, cancel, no-show and complete each write a timeline event, and the history survives |
| 7 | Reminders and payment policy | Each reminder sends once in the client's local time, and a deposit's state follows the booking through cancellation |
| 8 | Reports | No-show rate and revenue match a hand count from the bookings table |
| 9 | Handover audit | Data survives a restart, every mutation is checked on the server, and a concurrent-booking test passes |
That sequence is the Basic build order in our Appointment Booking planner. Waitlists, intake forms, packages, recurring bookings and multi-location come after step 9, one at a time.
The data model, briefly
| Table | Holds |
|---|---|
| business, location | Name, IANA time zone (for example America/Chicago), policies |
| staff, resource | People and the rooms, chairs or equipment a service needs |
| service | Duration, buffer before and after, price, deposit rule |
| service_eligibility | Which staff (and which resources) can perform which service, with optional per-staff duration or price |
| availability_rule | Recurring weekly hours in local wall-clock time, with an effective date range |
| availability_exception | Days off, holidays, one-off extra hours, blocked time |
| client | Contact details, consent, no-show history |
| booking | Client, service, staff, resource, start and end instants, status, source |
| booking_event | Every status change with actor and reason |
| notification_outbox | Each queued confirmation or reminder, with channel and delivery status |
| payment | Deposit, charge or refund, linked to a booking, with provider reference |
Two things matter more than the rest. Availability is rules, not slots. And a booking stores instants in UTC while the rules it was checked against are stored in local time with a named zone.
The hard parts
Availability is rules plus exceptions, not a table of free slots
The tempting design is a slots table: pre-generate every 30-minute slot for the next three months
and mark them booked. It breaks as soon as anything is real. A 90-minute service needs three
adjacent slots. A staff member changes their Tuesday hours and three months of rows are wrong. A
holiday has to be deleted from every staff member's rows. Two services with different lengths can't
share a grid.
Store what the business actually decides instead: weekly rules ("Maria works Tuesday 09:00 to 17:00, from 1 March"), exceptions ("Maria is off 24 December"; "open until 20:00 on 23 December"), and the bookings that already exist. Slots are computed on request:
- Take the rules for that staff member and date, in the location's time zone.
- Apply exceptions.
- Subtract existing bookings, each widened by its buffers.
- Subtract anything inside the minimum notice or beyond the booking window.
- Cut what's left into start times that fit the service's full duration, plus its buffer.
- For "any available staff", repeat per eligible person and remember which one owns each slot.
Then run the same function again when the client presses Book. A slot shown at 10:02 can be gone by 10:04, and the submit path must not trust what the page displayed. The planner's research names this explicitly: displayed slots and submit-time validation come from the same engine, and an "any provider" booking saves the provider attached to the chosen slot, not whoever is first in the list.
Time zones and daylight saving
Store booking start and end as instants (timestamptz in Postgres). Store availability rules as
local wall-clock times plus the location's IANA zone name, never as a UTC offset. "09:00 in
Europe/London" is 08:00 UTC half the year and 09:00 UTC the other half. A rule saved as "08:00 UTC"
quietly moves the business's opening hour when the clocks change.
Three cases to test:
- The spring-forward day. In zones that observe daylight saving, one local hour does not exist. A rule starting at 02:30 that day has no matching instant, and your generator must skip it rather than crash or invent one.
- The fall-back day. One local hour happens twice. A booking at 01:30 is ambiguous unless you stored the instant.
- The client in another zone. Show times in the client's zone on the booking page, and say which zone. Render reminders in the zone of the person receiving them. A reminder that says "10:00" without a zone, sent to someone who travelled, is how people miss appointments.
Use your language's time zone library with the IANA database. Don't do offset arithmetic by hand.
Double-booking under concurrency
The common bug: two requests check availability at the same moment, both find the slot free, and both write a booking. Each request was correct on its own. Application code that does "check, then insert" cannot prevent it, because the check and the write are separate steps.
Put the guarantee in the database. In Postgres, an exclusion constraint rejects any two rows whose time ranges overlap for the same staff member. This follows the pattern in the Postgres range types documentation:
CREATE EXTENSION IF NOT EXISTS btree_gist;
CREATE TABLE booking (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
staff_id bigint NOT NULL REFERENCES staff(id),
service_id bigint NOT NULL REFERENCES service(id),
status text NOT NULL,
-- start of the pre-buffer to end of the post-buffer, set by the app
blocked tstzrange NOT NULL,
EXCLUDE USING gist (staff_id WITH =, blocked WITH &&)
WHERE (status IN ('pending', 'confirmed'))
);
The second insert fails with a constraint violation, which your code turns into "that time was just
taken, here are the nearest free slots". The WHERE clause means cancelled and no-show bookings
stop blocking time. If a service also needs a room, add the same constraint on a booking-resource
table keyed by resource. Ranges default to including the start and excluding the end, so a 10:00 to
11:00 booking does not collide with one starting at 11:00.
If you are not on Postgres, the equivalent is a transaction that locks the staff member's row (or a per-staff-per-day lock) before checking and inserting. Name the mechanism in your spec. If you can't name it, the system is a prototype.
Buffers
Buffers are why the constraint above uses a blocked range rather than the appointment time. A
45-minute colour service with 15 minutes of clean-up blocks an hour of the stylist's day, but the
client sees 45 minutes. Store buffer-before and buffer-after on the service (some businesses need
setup time before, not just clean-up after), allow a per-staff override if your business has it, and
make the slot generator and the constraint use the same widened range. If they disagree, the page
will offer a slot the database refuses.
Salons add a twist: processing time, where the stylist is free for 30 minutes while colour develops. That is a booking with a gap the staff member can sell, and the salon and spa booking system piece covers why generic schedulers get it wrong.
Cancellations, no-shows and reminders
A booking is not a row with a cancelled boolean. It is a small state machine (pending, confirmed,
rescheduled, cancelled, no-show, completed) with a timeline of who changed what and why. Disputes
over a late-cancellation fee are settled by that timeline, so write it from day one.
Make the policy data, not code: the cancellation window, whether a late cancellation forfeits the deposit, how many no-shows before a client must prepay. Hard-coded 24-hour windows are one of the failure modes the planner research lists by name.
Reminders go through an outbox table, not a direct call from the booking request. A scheduled job picks up due rows, sends them, and records delivery status. When a booking is rescheduled, the old reminder is cancelled and a new one queued; test that the client does not get both. Email reminders are cheap. Text reminders cost per message and are the ones people read, so decide early whether you need them, and keep three things apart: automated reminders, a two-way inbox, and marketing messages. They are different features that buyers often expect under one "SMS" label.
Payments and deposits
79 of the 92 systems we counted handle payments, deposits or no-show fees, because empty chairs are the revenue leak this software exists to plug. Use your payment provider's hosted checkout so card details never touch your server, and store only the provider's reference. Then model the money as part of the booking lifecycle: deposit pending, deposit paid, refunded, forfeited. Each transition follows the cancellation policy.
Two traps. A card authorisation hold expires after a limited period set by the card network and your processor, so a hold placed today for an appointment in a month may lapse; check your processor's rules before choosing hold over charge. And a booking must not show as confirmed if the deposit it requires failed. Until real payments are wired and tested, label them as test mode in the interface.
Build it with an AI coding agent
Building an appointment booking app with a coding agent is realistic now. A model will produce something with a calendar and a booking page in a day. What it will not do unprompted is everything in the previous section, because "build me a booking app" asks for none of it.
What to hand the agent. A spec that has already made the decisions: the booking type, the out-of-scope list, the data model with availability as rules and exceptions, the race-safety mechanism by name, the lifecycle states, and the build order with a "done when" check per step. Tell it to stop at each check.
What to check after each step. The "done when" column above, run by you rather than reported by the agent. In particular: book the same slot from two browser windows at once; book across a clock change; sign in as staff and call an owner-only endpoint directly; restart the server and confirm the bookings are still there.
What our build-off caught. Seven appointment apps were built from our planner by Claude Opus 4.8 (four runs), Composer 2.5, GLM 5.2 and Kimi 2.7. The runs were not fully scope-identical, so the scores on the build-off board are a read of each build, not a model ranking. They ranged from 93 to 52. What separated them:
- The top build, a salon back office, carried every requested screen through to the end and passed every automated check, but payments and notifications ran in test mode and needed live accounts before launch. A build that says so is the good outcome.
- The fitness build proved that two people grabbing the last place at once cannot both get it. That is the concurrency test above, done properly.
- One build sidestepped payments, intake and checkout entirely and handed over a tidy app as a result. A clean handover of a narrow scope can look like quality.
- One failed its own automated checks, left test data on customer-facing screens, overflowed on a phone, and shipped handover notes that contradicted each other about what was ready.
- The lowest had weak and inconsistent proof that logins, permissions and exports worked, with internal test labels visible to customers.
Earlier dogfood runs of the same planner taught us something too: builds that looked finished were still prototype-shaped underneath. The planner now refuses to call a build beta until it proves submit-time slot validation, conflict checks on overlapping intervals rather than equal start times, and a named race-safety mechanism. All seven apps are live in the appointment booking demos, including the low scorers.
The mistakes that show up every time
- A slots table. Pre-generated slots that go stale the first time hours change.
- Overlap checked by start time. A 10:30 booking slips past a 10:00 to 11:00 booking because the start times differ.
- Check-then-insert with no constraint. Fine in a demo, two clients in one chair on a busy Saturday.
- Offsets instead of zone names. Opening hours that move by an hour twice a year.
- Status as a boolean. No history, so no way to settle a no-show dispute.
- Reminders sent inline. A slow email provider makes booking slow, and a reschedule sends two reminders.
- Permissions only in the interface. Hiding a button is not a permission. The server has to refuse the request.
What booking system development costs
These are rough ranges with stated assumptions, not quotes. Assumptions: one location, one-to-one appointments, a handful of staff, email reminders, deposits through a hosted payment provider, and a developer who knows the stack. They exclude visual design, data migration and legal review.
| Path | Rough effort | Ongoing cost to expect |
|---|---|---|
| Embed a hosted scheduler | An hour to a day | The vendor's subscription and any per-staff pricing |
| Customise a self-hosted scheduler | One to three weeks | Hosting, updates, backups, email and text delivery |
| Custom build with an AI coding agent and a spec | Two to four weeks to something customers can use | Hosting, email and text delivery, payment fees |
| Custom build by hand | Six to twelve weeks | The same, plus your own maintenance time |
Online booking system development that skips the hard parts is quick and then expensive, because customers find the double-booking and time zone bugs.
Appointment booking system as a student project
If this is a course project, scope it to one business, one-to-one appointments and one time zone, and spend your effort on two things examiners notice: availability computed from rules, and a database constraint that blocks overlapping bookings, with a test showing two simultaneous requests where only one succeeds. That is more impressive than ten screens.
FAQ
How much does it cost to build a booking system? Embedding a hosted scheduler costs the subscription. A custom build is weeks of developer time (see the rough ranges above) plus hosting, message delivery and payment fees.
How long does it take to build an online booking system? A first version with a booking page takes days with a coding agent. A version you would let customers rely on, with concurrency, time zones, reminders and deposits tested, takes weeks.
What tech stack should I use? Any mainstream web stack works. The decision that matters is the database: Postgres gives you range types and exclusion constraints, which make double-booking structurally impossible. Add a job runner for reminders and a payment provider with hosted checkout.
Can I build a booking system with no-code tools? Yes, for simple cases, and an embedded scheduler is the quickest of all. No-code tools struggle with shared resources, per-staff service rules and concurrency guarantees, which is where custom builds start to earn their cost.
Should I build my own or use Calendly? Use a hosted scheduler if one person takes one-to-one bookings with simple rules. Build when your availability rules, resources or customer records don't fit its settings. Clinics have extra reasons, covered in patient appointment scheduling software.
The shortcut
The hard parts of a booking system are known. They are just invisible until a client turns up to a chair that is already taken. The Appointment Booking planner puts them in front of your coding agent before it writes code: a discovery interview that picks your booking type, the counted inventory from 92 live systems, the nine-step build order with a hard gate per step, and acceptance tests for submit-time validation, interval overlap, race safety and source-of-truth reports.
Seven apps were built from it, and the results, the weak ones included, are on the build-off board.