Scheduling is one of those features that seems simple until you build it. "Let users book a time slot" sounds like a weekend project. Then you discover time zones, daylight saving transitions, recurring events, calendar sync conflicts, buffer times between appointments, multi participant availability, and cancellation policies. Suddenly you are deep in one of the most nuanced areas of application development.
We have built scheduling systems for booking platforms, professional services tools, and SaaS products. The patterns that work are well established, but the implementation details matter enormously. A scheduling bug that double books a client or loses an appointment destroys user trust in ways that are hard to recover from.
Time Zone Architecture: The Foundation
Every scheduling system must make a foundational decision about how it stores and processes times. Get this wrong and every other feature is built on a broken foundation.
The rule is simple: store everything in UTC, display in local time. Every datetime in your database should be a UTC timestamp. When you display a time to a user, convert it to their local time zone. When a user picks a time, convert their local selection to UTC before storing it.
This sounds obvious, but the details are tricky. A user in New York selects "2:00 PM" for an appointment. Your system stores that as 18:00 UTC (during Eastern Daylight Time) or 19:00 UTC (during Eastern Standard Time). The same "2:00 PM" wall clock time maps to different UTC values depending on the date. Your conversion logic must account for the specific date of the appointment, not just the user current offset.
Never store time zone offsets like "UTC minus 5." Offsets change with daylight saving time. Store the IANA time zone identifier (like "America/New_York") and compute the offset for each specific datetime. Libraries like Luxon (JavaScript) and pytz (Python) handle this correctly. The native JavaScript Date object does not handle it well, avoid relying on it for time zone conversions.
Recurring events make this even more complex. A weekly meeting at "Tuesday 10:00 AM Eastern" should always be at 10:00 AM Eastern, even when the UTC offset changes during DST transitions. This means you cannot just store the first occurrence in UTC and add 7 days repeatedly. You must store the recurrence rule with the user local time and time zone, then compute each occurrence UTC value individually.
The Availability Model
Scheduling systems need to know when someone is available. Availability is typically defined as a combination of recurring schedules and overrides.
A recurring schedule defines the default pattern: "Available Monday through Friday, 9:00 AM to 5:00 PM Eastern, with a lunch break from 12:00 PM to 1:00 PM." This is stored as a set of rules, not as individual time slots. Each rule specifies a day of the week, start time, end time, and time zone.
Overrides handle exceptions: "Not available on December 25th" or "Available on Saturday the 14th from 10:00 AM to 2:00 PM." Overrides take precedence over the recurring schedule for their specified date.
The availability computation works like this: for a given date range, generate time slots from the recurring schedule, apply overrides (both blocking and adding availability), subtract existing bookings, and return the remaining available slots. This computation should be fast and cacheable because it runs on every calendar view and every booking request.
Slot duration and buffer times add granularity. A 30 minute appointment type with 15 minute buffers means each booking actually blocks 60 minutes (15 minute buffer, 30 minute appointment, 15 minute buffer). The available slots calculation must account for these buffers.
Calendar Sync: Google and Outlook
Most scheduling products need to sync with external calendars. Users expect their booked appointments to appear on their Google Calendar or Outlook, and they expect their existing calendar events to block out availability.
Google Calendar uses OAuth 2.0 for authentication and a REST API for reading and writing events. The key operations are: list events in a date range (to check for conflicts), create events (when a booking is confirmed), and update or delete events (when bookings are modified or cancelled). Google also provides push notifications via webhooks that tell your system when a user calendar changes, so you can update availability in near real time instead of polling.
Microsoft Outlook uses Microsoft Graph API with OAuth 2.0. The patterns are similar, list events, create events, subscribe to changes, but the API surface is different. Microsoft uses subscription based webhooks that require periodic renewal (every few days) and a validation handshake when first registered.
CalDAV is the open standard for calendar access. It is supported by Apple Calendar, Fastmail, and many other providers. CalDAV is more complex to implement than the Google or Microsoft APIs because it uses XML (specifically WebDAV with iCalendar extensions), but it covers a broad range of calendar services with a single integration.
For two way sync, the architecture involves:
1. Inbound sync: Periodically fetch events from the user external calendar and mark those times as unavailable. Use push notifications where available to minimize latency.
2. Outbound sync: When a booking is created, create a corresponding event on the user external calendar. Include relevant details (attendee name, meeting link, notes) in the calendar event.
3. Conflict resolution: When a sync reveals a conflict (the user accepted a meeting on their external calendar that overlaps with an available slot), immediately block that slot and, if a booking was already made, trigger a rebooking or cancellation flow.
The sync system must be idempotent and resilient. Calendar APIs are rate limited, occasionally down, and sometimes return stale data. Store a sync token or last sync timestamp per user and use it to fetch only changes since the last sync. Handle API failures with exponential backoff and a dead letter queue for events that repeatedly fail to sync.
Booking Flow and Conflict Detection
The booking flow is where race conditions live. Two users viewing the same calendar see the same available slot. Both click "Book." Only one should succeed.
The solution is optimistic locking at the database level. When a booking request comes in, start a database transaction. Query for existing bookings that overlap the requested time range. If none exist, insert the new booking and commit. If a conflict exists, roll back and return an error.
In PostgreSQL, this looks like a SELECT with a row level lock (FOR UPDATE) on the availability record, followed by an INSERT into the bookings table. The transaction isolation level must be at least READ COMMITTED (which is the PostgreSQL default) to prevent phantom reads.
```sql
BEGIN;
SELECT id FROM bookings
WHERE provider_id = $1
AND status = 'confirmed'
AND tsrange(start_time, end_time) && tsrange($2, $3)
FOR UPDATE;
-- If no rows returned, insert the new booking
INSERT INTO bookings (provider_id, client_id, start_time, end_time, status)
VALUES ($1, $4, $2, $3, 'confirmed');
COMMIT;
```
PostgreSQL range types and the overlap operator (&&) make this query both clean and efficient. Add a GiST index on the time range for fast conflict checks.
For high traffic systems, you may also want a distributed lock (using Redis or a similar system) to prevent thundering herd problems where dozens of concurrent requests all pass the database check simultaneously. The database transaction handles correctness, the distributed lock handles efficiency.
Recurring Bookings and Series
Recurring bookings add significant complexity. A client books a "weekly therapy session every Tuesday at 2:00 PM for 12 weeks." Your system needs to create 12 individual bookings, each of which can be independently cancelled or rescheduled.
Store the recurrence rule (using the RFC 5545 RRULE format, the same format Google Calendar uses) on a booking series record. Generate individual booking instances from the rule. Each instance links back to the series but has its own status, notes, and modification history.
When the user modifies the series ("change all future sessions to 3:00 PM"), update the recurrence rule and regenerate future instances. When the user modifies a single instance ("cancel just next Tuesday"), mark that specific instance as an exception.
This is the same pattern Google Calendar uses internally, and it is the most flexible approach for handling the full range of recurrence modifications users expect.
Notifications and Reminders
Scheduling systems require reliable notifications. At minimum:
Booking confirmation sent immediately when a booking is created. Include the date, time (in the recipient time zone), location or meeting link, and cancellation/reschedule instructions.
Reminders sent at configured intervals before the appointment. 24 hours and 1 hour before are standard. These reduce no show rates by 30 to 50% based on our experience.
Cancellation and modification notices sent to all participants when a booking changes.
Reminders require a scheduled job system. When a booking is created, schedule reminder jobs for the configured times before the appointment. Use a reliable job queue (not cron on a single server) that guarantees execution. If a booking is cancelled, delete the pending reminder jobs.
Email is the baseline channel. SMS reminders have higher engagement rates but add cost and complexity (Twilio or a similar provider, opt in/opt out compliance, international number formatting). Push notifications work for products with mobile apps.
Multi Participant Scheduling
Finding a time that works for multiple people is exponentially harder than single participant scheduling. The system needs to compute the intersection of availability across all participants.
For two participants, query each person available time slots and find the overlap. For three or more, intersect iteratively. The computational cost grows with participant count, so set reasonable limits (most products cap at 10 to 15 participants for automated scheduling).
Display the results as a ranked list of available slots, with the "best" options first. "Best" can mean different things: the earliest available slot, the slot with the most buffer time around it, or the slot that is within everyone preferred hours rather than just their technically available hours. Let the organizer choose.
For teams evaluating whether to build custom or use an existing scheduling tool, the deciding factor is usually integration depth. If scheduling is a feature within a larger product (like a marketplace with appointment booking), custom is almost always the right call. Embedded scheduling solutions like Calendly or Cal.com can work for simple use cases but limit your ability to customize the experience and keep data within your system.
Launch Checklist
Before shipping scheduling features, verify these non negotiable items:
Time zone conversion tests for dates around DST transitions. Test March and November specifically for US time zones.
Conflict detection under concurrent load. Simulate 10 simultaneous booking attempts for the same slot and confirm only one succeeds.
Calendar sync error handling. Disconnect a linked calendar and verify your system degrades gracefully (shows a warning, does not crash).
Reminder delivery. Confirm reminders fire at the correct local time, not UTC time.
Cancellation cascade. When a booking is cancelled, verify all associated calendar events, reminders, and notifications are cleaned up.
Scheduling is infrastructure that your users rely on for real world commitments. A bug in a scheduling system means someone shows up to an empty office or a provider double books their afternoon. The stakes are higher than most features.
If you are building scheduling into your product, contact us to discuss your requirements. We have shipped these systems and can help you avoid the time zone pitfalls, sync edge cases, and concurrency bugs that make scheduling one of the hardest features to get right.