System Design: Appointment Scheduling — Time Slot Management, Booking Conflicts, and Reminders

Requirements and Scope

An appointment scheduling system allows users (customers) to book time slots with service providers (doctors, barbers, consultants). Core requirements: providers configure their availability (working hours, breaks, buffer time between appointments); customers view available slots and book; double-booking is prevented; both parties receive reminders. Scale: a SaaS scheduling platform like Calendly serves millions of bookings per day. Key challenges: preventing concurrent double-booking, handling time zones correctly, and scaling availability queries across many providers.

Data Model

Provider: provider_id, name, timezone, service_duration_minutes, buffer_minutes (gap between appointments), max_advance_booking_days. ProviderAvailability: availability_id, provider_id, day_of_week (0=Mon, 6=Sun), start_time, end_time (local time, recurring weekly). ProviderException: exception_id, provider_id, date, is_blocked (true=provider unavailable; false=custom hours), custom_start, custom_end. Appointment: appointment_id, provider_id, customer_id, start_time (UTC), end_time (UTC), status (CONFIRMED, CANCELLED, COMPLETED, NO_SHOW), created_at, notes. ReminderLog: log_id, appointment_id, type (EMAIL, SMS), sent_at, status (SENT, FAILED).

Slot Generation and Availability Query

Available slots are generated on-the-fly, not stored. Given provider P and date D: (1) Get P’s weekly availability for the day of week of D. (2) Check ProviderExceptions for date D — override or block the day. (3) Expand into potential slot times: start_time, start_time+duration, start_time+duration+buffer, … until end_time. (4) Subtract booked appointments: SELECT start_time, end_time FROM appointments WHERE provider_id=:p AND DATE(start_time AT TIME ZONE p.timezone)=:d AND status IN (‘CONFIRMED’). (5) Return the remaining slots. Slot conflicts: a slot is unavailable if any booked appointment overlaps with it (including buffer time). A slot [s, e+buffer] conflicts with booking [bs, be] if s bs. Caching: cache the availability for a provider + date for 1 minute. Invalidate when an appointment is booked or cancelled. For high-demand providers (popular doctors): cache aggressively; show “temporarily unavailable” if cache miss during peak load rather than generating slots on every request.

Booking with Conflict Prevention

class BookingService:
    def book_appointment(self, provider_id: int, customer_id: int,
                         start_time: datetime, duration: int,
                         idempotency_key: str) -> Appointment:
        # Idempotency check
        if cached := self.idempotency_cache.get(idempotency_key):
            return cached

        end_time = start_time + timedelta(minutes=duration)
        end_with_buffer = end_time + timedelta(
            minutes=self.get_provider_buffer(provider_id)
        )

        with self.db.transaction():
            # Lock all appointments for this provider on this day
            self.db.execute(
                "SELECT pg_advisory_xact_lock(:lock_id)",
                lock_id=hash(f"{provider_id}:{start_time.date()}")
            )

            # Check for conflicts
            conflicts = self.repo.find_conflicts(
                provider_id, start_time, end_with_buffer
            )
            if conflicts:
                raise SlotUnavailableError()

            appt = Appointment(provider_id=provider_id,
                                customer_id=customer_id,
                                start_time=start_time,
                                end_time=end_time,
                                status=AppointmentStatus.CONFIRMED)
            self.repo.save(appt)

        self.idempotency_cache.set(idempotency_key, appt, ttl=86400)
        return appt

PostgreSQL advisory locks (pg_advisory_xact_lock) provide a lightweight per-provider-day lock without row-level contention. The lock key is derived from (provider_id, date) — serializes bookings for the same provider on the same day, while allowing concurrent bookings for different providers.

Reminders and Notifications

Reminder schedule: 24 hours before + 1 hour before the appointment. A scheduled job runs every minute: SELECT a.* FROM appointments a WHERE a.status=’CONFIRMED’ AND a.start_time BETWEEN NOW()+23h AND NOW()+25h AND NOT EXISTS (SELECT 1 FROM reminder_logs rl WHERE rl.appointment_id=a.appointment_id AND rl.type=’EMAIL_24H’). Send reminder, log to reminder_logs. Idempotent: the NOT EXISTS check prevents duplicate sends even if the job runs twice. Timezone display: reminders show the appointment time in the customer’s timezone (not UTC). Store customer_timezone on the appointment at booking time (from the booking request). Time zone conversion: use pytz or dateutil for correct DST-aware conversion. Cancellation window: define a cancellation policy (e.g., free cancellation up to 24 hours before). After the window: cancellation may incur a fee. Check the policy in the CancelAppointment handler before proceeding.

Asked at: Stripe Interview Guide

Asked at: Airbnb Interview Guide

Asked at: Atlassian Interview Guide

Asked at: Coinbase Interview Guide

Scroll to Top