The booking screen is the easy part of an appointment system. A calendar and a "Book now" button take days to build and look correct in a demo. What breaks shows up two months into live operation: two patients in the same room at 5pm, a customer in Kuwait who arrives an hour after his slot, a weekly session that slides by an hour at the end of April, and a messaging bill that doubles for no visible reason.
Behind each of those failures sits a specific design decision, and most of them are taken in the first week of the project. What follows are the cases that break an appointment booking system after launch, and where each one is actually settled: in the interface, in the database, or in a table your receptionist edits.
Two bookings on the same slot
The usual path has three steps: the system reads the available times, the customer picks one, the system saves the booking. Seconds pass between the first step and the third, and in those seconds another customer can walk the same path and reach the save a fraction of a second earlier or later. The check that ran in the interface was correct when it ran, and stale by the time the row was written.
A condition in application code does not close this. The MySQL manual says it plainly: "the regular SELECT statement does not give enough protection. Other transactions can update or delete the same rows you just queried." Its answer is a locking read. SELECT … FOR UPDATE locks the rows and their index entries as if you had issued an UPDATE, so the second transaction waits for the first to finish and then sees the truth.
On PostgreSQL you have a sharper tool: the exclusion constraint. The PostgreSQL documentation uses a meeting-room reservation as its own example: a table with a room column and a time-range column, carrying a constraint that rejects any two rows where the room is equal and the ranges overlap (after enabling the btree_gist extension, which lets a plain scalar column join a range exclusion). The second overlapping row is refused by the database itself with an error saying the key conflicts with the exclusion constraint. The rule lives in the table rather than in code, so it survives a bulk import, an admin screen where somebody forgot to add the check, and a walk-in registered by hand at the same moment. Adding the constraint creates the required index automatically, so you pay nothing extra on reads.
If you are on MySQL without range types, the practical pattern is to store appointments on a fixed grid (quarter-hour slots, say) with a unique index on resource plus start time. The fixed grid costs you flexibility for services of different lengths, but it moves conflict prevention from the application into the database.
One door is still open: the customer who taps "Confirm" twice, or loses signal and resends the request. That is what an idempotency key is for. The client generates a unique key with the request, and the server stores the outcome of the first request carrying that key and replays it for any retry instead of performing the operation again. Stripe's documentation suggests a version 4 UUID and warns against putting personal data in the key.
The acceptance test here is simple: fire two concurrent requests at the same slot, and require that exactly one succeeds and the other receives a message a human can read rather than a raw database error.
A single resource is rarely single
An imaging session needs a technician, a room and a machine at the same moment. A laser session needs a specialist and a cabin. A private lesson needs a tutor and a hall. If the system was built on the idea that a booking belongs to "the doctor" alone, it will happily place two different doctors in one room, and you will learn about it from a complaint rather than a report.
The fix is to attach a booking to a list of resources instead of one resource, and to apply the non-overlap rule to each resource separately. A second rule follows and is easy to forget: cancelling a booking has to release every resource it held, and rescheduling has to confirm that all of them are free at the new time before it commits.
Time: what you store is not what you show
The uncontroversial rule is to store the instant in UTC and render it in the viewer's zone. A customer booking from London sees his appointment in London time, the receptionist in Jeddah sees the same appointment in Jeddah time, and the value in the database is one value.
One detail gets missed. PostgreSQL's timestamp with time zone type is stored internally as UTC and does not retain the original time zone. If you later need to know that this appointment was booked in Riyadh time, keep the zone identifier in its own column. Make it a full name such as Asia/Riyadh rather than an abbreviation, because the documentation draws the line clearly: an abbreviation is a fixed offset from UTC, while a full name carries daylight-saving rules with it.
Egypt and the Gulf do not move together
The time zone database that nearly every system relies on, the IANA database, records two open-ended rules for Egypt in force since 2023: summer time starts on the last Friday of April at midnight and ends at 24:00 on the last Thursday of October. Against that, Riyadh, Doha and Dubai are recorded at a fixed offset with no daylight-saving rule at all.
For anyone running branches in Cairo and Riyadh, the gap between them is not constant. Cairo is UTC+2 and Riyadh UTC+3 for most of the year, and while Egyptian summer time is in force the two branches read the same clock. In 2026 the boundaries fall on 24 April and 29 October. Any report, sync or "one day before the appointment" calculation that assumes a fixed gap will be wrong for that whole stretch.
Each transition brings a broken hour with it. In spring an hour of the day does not exist, and in autumn an hour happens twice. The standard that calendars rely on settles both cases: a repeated local time is read as the first occurrence, and a local time that does not exist is interpreted using the offset before the gap. What matters is that your system knows the rule instead of crashing or creating an appointment at a time that never happens.
A recurring appointment is not an instant
Physiotherapy every Tuesday at 5pm is not "a UTC instant plus seven days". Store it that way and the fourth session lands at 4pm or 6pm depending on which side of a transition it falls. RFC 5545 states that the start of the series "should be specified as a date with local time and time zone reference to make sure all the recurrence instances start at the same local time regardless of time zone changes."
In other words: store "Tuesday 17:00 in Africa/Cairo" and derive the instants from it, not the reverse.
The Hijri calendar: five calendars, not one
"Add the Hijri calendar" sounds like a day's work. The problem is that Hijri in software is not one calendar. Unicode's calendar registry, the reference that operating systems and date libraries build on, defines five identifiers: islamic; islamic-umalqura, the Umm al-Qura calendar; islamic-tbla and islamic-civil, two tabular arithmetic calendars that differ in their epoch; and islamic-rgsa, described as Saudi Arabia sighting. Picking one over another shifts the displayed date by a full day, which is enough for a customer to complain that his appointment is "written wrong".
If your audience is in Saudi Arabia, the identifier matching the official calendar is Umm al-Qura. But that only settles the display, and the harder problem sits elsewhere.
The start of Ramadan and the date of Eid al-Fitr are announced, not computed. In the Supreme Court announcement carried by the Saudi Press Agency in March 2026, the court states that its decision 204/H dated 29/8/1447 was what fixed the first day of Ramadan, and calls for the Shawwal crescent to be sighted on the evening of the twenty-ninth of Ramadan. The Gregorian date of Eid is therefore not known with certainty until a day beforehand.
Only one design rule survives that: a holidays and working-hours table the receptionist edits herself, with effective dates, instead of a rule written into code. It needs a written decision attached to it for what happens to bookings already made on a day that has just become a holiday. Are they cancelled automatically, shown to management for rescheduling, and who tells the customer?
Ramadan forces a second change in Saudi Arabia. Article 98 of the Labour Law reduces actual working hours for Muslims during Ramadan so that they do not exceed six hours a day or thirty-six a week. Your "weekly schedule" cannot be a single constant; it needs versions with effective periods, and the Ramadan version is one of them. The same business in Egypt follows a different schedule in the same month.
Cancellation, rescheduling and no-shows
The most commonly over-simplified thing in booking systems is the state of an appointment. A single boolean column named is_cancelled will not run a clinic. The states you usually need: tentative, confirmed, attended, no-show, cancelled by the customer, cancelled by the business, rescheduled. And every transition between two states carries three questions: who did it, when, and why.
The difference between cancelling and rescheduling is not a matter of wording. If rescheduling is implemented by deleting the old appointment and creating a new one, the customer's history disappears: how many times they moved a slot, how many times they did not show. The alternative is a booking that keeps its identifier and a log of its changes, with "no-show" recorded as a state rather than a deleted row, because that is what deposit rules and rebooking rules hang on.
The allowed cancellation window belongs on the server, not in the interface alone. A hidden "Cancel" button does not stop a direct call to the API.
Recurring bookings need two models rather than one: a series with a recurrence rule, and exceptions on specific dates. The standard separates them the same way, with a rule that generates occurrences and an exception list that removes some of them, and the exceptions take precedence over the rule. Anyone storing the series as thirty independent rows will end up editing thirty rows every time the patient's slot moves.
Exporting to the customer's calendar
Snaabble provides a tailored technical assessment to define the right stack & exact budget.
"Add to my calendar" is a small feature that fails quietly. The exported file follows the iCalendar standard, and three of its properties decide whether the sync stays sound:
- UID: the persistent, globally unique identifier for the event. It must stay the same for the life of the booking. A system that mints a new one on every edit leaves three appointments in the customer's calendar instead of one.
- SEQUENCE: a number that starts at zero and increments with every significant revision. Without it the customer's calendar may ignore your update because it cannot tell which version is newer.
- The time in DTSTART tied to a time zone identifier, for the reason explained above.
Cancellation has its own path. The companion scheduling standard (RFC 5546) requires a message with the CANCEL method carrying the same UID, requires SEQUENCE to be incremented, and requires RECURRENCE-ID when what you are cancelling is one instance of a series. Without that, the cancelled appointment stays in the customer's calendar, and the customer turns up.
Reminders: the meter that starts on 1 October 2026
WhatsApp reminders are requested in almost every booking project, and their cost line is the one most often misread in proposals.
The basis: a message leaving your system to a customer who has not written to you in the last twenty-four hours must be an approved template, and that is the situation of nearly every "your appointment is tomorrow" message. The template falls in the utility category, and Meta charges for it because it is sent outside the customer service window. That has not changed.
Three things change on 1 October 2026. We read Meta's own documentation (updated 10 September 2026) on 21 September 2026:
- Service messages become charged per message, at rates equal to utility and authentication rates in the same market. A service message is any non-template reply your system sends inside the twenty-four hour window, which opens and resets with each message from the customer.
- Against that, a monthly free tier of 1,000 delivered service messages per business phone number, shared between one-to-one and group deliveries, with nothing unused rolling over to the next month.
- Utility templates sent inside the twenty-four hour window become charged, after being exempt since 1 July 2025.
The documentation adds an operational note: if no payment method is registered on the business account, service messages inside the free tier are delivered and messages after it are not. A booking system that stops replying halfway through the month is an operations problem before it is a finance one.
The rate is set by the country calling code of the recipient's number, not by where your business sits. From Meta's published rate card effective 1 October 2026, in US dollars per delivered message:
| Market | Utility template and service message | Marketing template |
|---|---|---|
| Egypt | 0.0036 | 0.0644 |
| Saudi Arabia | 0.0107 | 0.0576 |
| United Arab Emirates | 0.0157 | 0.0576 |
| Qatar | 0.0120 | 0.0341 |
The arithmetic for a mid-sized clinic is calmer than people expect. A hypothetical example: a clinic in Egypt with six hundred appointments a month, sending one confirmation template at booking and one reminder the day before. That is 1,200 utility templates, which at Egypt's rate in Meta's rate card is about 4.3 dollars a month. Customer replies ("confirm" or "reschedule") open service messages that usually stay under the free tier.
The bill grows somewhere else: the marketing message. "We miss you, book now" to the same list is classified as a marketing template, and in Egypt it costs roughly eighteen times a reminder. The difference is not only financial. Egypt's Law 151 of 2020.pdf) requires prior consent and a clear opt-out for direct electronic marketing, and obliges you to keep electronic records proving consent for three years from the date of the last send (articles 17 and 18). Saudi Arabia's law requires consent and an opt-out mechanism in its article 25.
In practice: separate consent to receive reminders from consent to receive offers in your database, and store with each consent the exact wording the customer agreed to and its date. For template categories and their rules see WhatsApp chatbots for your store, and for the full rate card across markets and currencies see How much an AI chatbot costs.
One field that changes what your system is
"Reason for visit" or "requested specialty" is a field a designer adds in a minute. In Egypt that field moves your system from processing ordinary personal data to processing sensitive data.
Article 1 of Law 151 of 2020 defines sensitive personal data as data revealing mental, cognitive, physical or genetic health, alongside biometric and financial data, and states that children's data is sensitive in all cases. Article 12 prohibits a controller or processor from collecting, transferring, storing, processing or making available sensitive data "except with a licence from the Centre", and additionally requires explicit written consent from the data subject, and the guardian's consent where a child's data is involved. The penalty in article 41 is imprisonment of not less than three months plus a fine between five hundred thousand and five million Egyptian pounds, or one of the two.
In Saudi Arabia, the Personal Data Protection Law defines health data as any data relating to an individual's health condition, physical, mental or psychological, or to the health services provided to them, and lists it among sensitive data. Article 23 requires access to health data, including medical files, to be limited to the smallest possible number of staff and only as needed to deliver the service. Article 26 excludes sensitive data from marketing processing altogether.
Three design decisions follow directly. Permissions at field level rather than screen level, so the receptionist sees the name and the slot without the file. A reminder template that does not carry the reason for the visit, because the message passes through parties and channels you do not control. And an explicit question before any clinical field is added: do we need it on the booking screen, or in the medical record after the visit? For the obligations translated into tables and columns, see customer data protection in Egypt and Saudi Arabia.
Deposits: a time window nobody quotes
Free booking is easier on the customer, and a deposit is the tool used to cut last-minute cancellations. If you choose a deposit, two technically different paths are open.
The first is to charge the deposit immediately. The second is to place a hold on the card without capturing it (an authorization), then capture it on arrival or release it on cancellation. The second is cleaner for the customer, but it is governed by a time window many people do not know about: Moyasar's documentation states that an authorization must be captured within 14 days on the mada scheme, while other card schemes may allow longer, and that an uncaptured hold is eventually released.
Which means a held deposit does not work for an appointment three weeks out. Either you charge, or you write a different deposit policy for distant appointments.
The opposite direction is governed by a narrower window: captured payments can only be voided within roughly two hours of the transaction, after which a refund is required. The documentation advises preferring a void over a refund where possible, because it releases the hold instantly and avoids processing fees. So the cancellation policy written on your site is drafted after reading your provider's terms, not before. Terms differ between providers, so ask about them by name before you sign; for a comparison of the gateways available in Egypt see our payment gateways guide.
One last item that falls through: the payment webhook may arrive twice, or arrive after the customer has closed the page. Handle the notification against a unique transaction identifier so it cannot create a duplicate booking or charge twice, and confirm the booking from the webhook rather than from the browser's return.
When an off-the-shelf tool is enough
Building a custom appointment booking system has a price and not every business should pay it. A ready-made tool is sufficient and sensible if you have one resource type, one branch, one time zone, stable working hours, no health data, and no need to connect to your other systems.
Building becomes justified when one of these holds: a single appointment needs several resources free at the same moment; branches sit in different countries with different time zones and holidays; a booking has to move stock, issue an invoice or open a record in another system; the data you collect is sensitive and brings obligations a ready-made tool cannot discharge on your behalf; or your business rules change every few months.
For the general buy-versus-build comparison see off-the-shelf or custom business software, and for connecting bookings to sales and invoicing see POS system integration.
Five questions before you sign a proposal
Ask whoever is going to build it, and insist on a specific answer rather than a promise:
- How is double booking prevented? An acceptable answer names a constraint or a lock in the database, not "a check in the system".
- Where are appointments stored, and how are they shown to a customer abroad?
- Who edits the holidays and Ramadan hours, and does it need a developer?
- Which appointment states are supported, and does "no-show" appear in the customer's history?
- Who owns the WhatsApp Business account, who pays Meta's bill, and when is a message sent as a marketing template?
Sources
- MySQL 8.4 Reference Manual, Locking Reads
- PostgreSQL 18 documentation: Range Types, Constraints and Date/Time Types
- IANA Time Zone Database, the africa and asia files
- RFC 5545 (iCalendar) and RFC 5546 (iTIP)
- Unicode CLDR calendar identifier registry
- Saudi Press Agency: Supreme Court announcement on the Shawwal crescent, 1447 AH
- Saudi Ministry of Human Resources and Social Development: actual working hours
- Meta: Pricing on the WhatsApp Business Platform, updated 10 September 2026, with the rate card effective 1 October 2026
- Egypt's Law 151 of 2020.pdf), Official Gazette, issue 28 bis (E)
- Saudi Personal Data Protection Law
- Moyasar: Payment Flows and Stripe: Idempotent requests
All sources accessed on 21 September 2026.
If one of these cases has started showing up in a system you already run, or you are planning to build one from scratch, write to the Snaabble team and tell us what your customers book and how many branches and resources a single appointment occupies. You will get an initial plan with a time and cost estimate within 24 hours, free and with no obligation, through our custom software systems page.
Frequently asked questions
Can SMS replace WhatsApp for reminders?
The operational difference is that WhatsApp requires a pre-approved template for any message sent outside the twenty-four hour window, while SMS has neither templates nor a window. In return, SMS gives the customer no reply buttons to confirm or reschedule with. The practical decision is to support both channels with a preferred channel per customer, since a customer’s number may not be on WhatsApp at all, and to cost each channel separately because the providers are different.
Should the booking screen show the Hijri calendar alone?
Show both dates. The Gregorian date is what will appear in the customer’s phone calendar, in the exported iCalendar file and in the reminder message, while the Hijri date is what many customers in Saudi Arabia and the Gulf read. What matters is fixing the calendar identifier in settings, because a different identifier on one screen than another inside the same system shows two different dates for one appointment.
A salon booking system records no health data. Do the sensitive-data rules apply?
The licence requirement in article 12 of Egypt’s Law 151 of 2020 attaches to sensitive data specifically, health data among it. A name, a phone number and a session time are ordinary personal data, not sensitive. The law’s general obligations still stand: consent, retention periods, responding to data-subject requests, and breach notification. Note that some cosmetic services do involve recording a skin condition or an allergy, and at that point the classification changes.



