# Scheduling Architecture for SaaS Products: Common Questions Answered

> FAQ-style guide to scheduling architecture for SaaS products — state machines, multi-tenancy, API design, calendar sync, and scalability decisions.

- **Published:** 2026-03-19
- **Author:** Clixo
- **Reading time:** 7 min read
- **Tags:** scheduling, saas, architecture, faq, backend
- **Canonical URL:** https://clixo.sh/blog/scheduling-architecture-for-saas-products

Product teams building scheduling into a SaaS product face a set of recurring architecture questions that do not have obvious answers until you have built one of these systems before. The questions are predictable; the mistakes are also predictable. This post answers the ones that come up most consistently, directly.

## Scheduling Architecture for SaaS Products: Core Questions

### Should scheduling be a separate service or embedded in the core product?

For most early-stage SaaS products, embedding scheduling in the core product is the right call. A separate service adds deployment complexity, inter-service latency, and an additional failure boundary without much benefit until the scheduling domain is genuinely large enough to justify independent scaling.

Separate the scheduling domain when: it has meaningfully different scaling characteristics than the rest of the product, you need to expose a standalone scheduling API to third parties, or the team working on it needs to deploy independently.

Until then, a well-bounded module within your existing application — with a clear domain model and no circular dependencies — is easier to operate and faster to evolve.

### What state machine should bookings use?

A booking's lifecycle is a state machine. Define it explicitly rather than letting status values accumulate informally.

A minimal production state machine:

- `pending` — booking created but not yet confirmed (e.g., awaiting payment)
- `confirmed` — booking confirmed and on the calendar
- `cancelled` — cancelled by the customer or provider
- `completed` — appointment has occurred
- `no_show` — customer did not attend

Valid transitions:

- `pending` → `confirmed` (payment succeeds)
- `pending` → `cancelled` (payment fails or customer cancels during hold)
- `confirmed` → `cancelled` (cancellation before appointment)
- `confirmed` → `completed` (appointment occurs)
- `confirmed` → `no_show` (appointment time passes without check-in)

```mermaid
stateDiagram-v2
  [*] --> pending
  pending --> confirmed : payment succeeds
  pending --> cancelled : payment fails or customer cancels
  confirmed --> cancelled : cancellation before appointment
  confirmed --> completed : appointment occurs
  confirmed --> no_show : appointment time passes
  cancelled --> [*]
  completed --> [*]
  no_show --> [*]
```

Store every status transition with a timestamp and the actor who triggered it. This creates an audit trail and makes debugging and customer support significantly easier.

### How should multi-tenant scheduling data be isolated?

For a SaaS product where each customer has their own resources and booking flows, the standard options are:

- **Row-level isolation**: All tenants share a schema, with a `tenant_id` column on every table. Fast to implement, but requires discipline to ensure every query filters by `tenant_id`. A row-level security policy in PostgreSQL enforces this at the database level.
- **Schema-per-tenant**: Each tenant gets a dedicated schema in the same database. Better isolation, easier per-tenant analytics, but adds complexity to migrations and connection management.
- **Database-per-tenant**: Maximum isolation, easiest to scale per tenant independently, but expensive and operationally complex.

For most SaaS scheduling products, row-level isolation with PostgreSQL row-level security is the practical starting point. Move to schema-per-tenant when tenant count or per-tenant data volumes make shared-schema query performance a concern.

### What is the right API shape for a scheduling system?

Design the API around bookings as the core resource. Standard endpoints:

- `GET /availability` — returns available slots for a resource within a time window. Accepts resource ID, start/end datetime, and duration.
- `POST /bookings` — creates a booking. Accepts resource ID, start datetime, customer details. Must be idempotent via a client-provided idempotency key.
- `GET /bookings/:id` — returns a single booking.
- `PATCH /bookings/:id` — updates a booking (reschedule, status change).
- `DELETE /bookings/:id` — cancels a booking.

Return all datetimes in ISO 8601 with explicit UTC offset. Expose a webhook system so integrators can react to booking events without polling.

Avoid RPC-style endpoints like `POST /confirm-booking` or `POST /cancel-booking` in favor of status transitions via `PATCH /bookings/:id` with a `status` field — it is more consistent and easier to document.

### How should the system handle calendar sync without polling?

Calendar sync via polling is expensive in API quota and slow to reflect changes. Use push notifications from calendar providers instead.

Google Calendar supports watch channels: you register a webhook URL and a channel ID, and Google sends a notification when any event in the watched calendar changes. The notification does not include the changed data — it is a signal to re-fetch the relevant free/busy window.

Microsoft Graph subscriptions work similarly. Both expire (Google channels expire after at most a week; Graph subscriptions expire after a few days to a few months depending on resource type). Build a renewal job that re-registers subscriptions before they expire.

When a notification arrives, invalidate your availability cache for the relevant resource and re-fetch free/busy data in a background job. Do not re-fetch synchronously in the webhook handler — acknowledge the webhook immediately and process asynchronously.

### How do you handle availability across multiple time zones?

Store everything in UTC. This is non-negotiable. The bugs introduced by storing local times without time zone context compound over time and are difficult to fix after data exists in production.

For display, store each resource's IANA time zone identifier (e.g., `America/Los_Angeles`) and convert to local time at the display layer. Do the same for customers — store their time zone preference and use it to render booking times in their local context.

When generating available slots for display, convert the UTC slot boundaries to the viewer's time zone. If the viewer's time zone differs from the resource's, the displayed times should reflect the viewer's context with a clear indication of the resource's time zone where relevant (e.g., "1:00 PM your time / 4:00 PM provider time").

### How should the system scale when booking volume grows?

The availability computation endpoint is typically the performance bottleneck. Two approaches:

**Cache aggressively**: Compute and cache available slots for each resource and time window. Invalidate when bookings are created, cancelled, or rescheduled, or when calendar sync pushes a change. A short cache TTL (30–60 seconds) is usually sufficient and dramatically reduces load.

**Read replicas**: Route availability queries to a read replica. Since availability reads are the high-volume, low-write-sensitivity path, read replicas are well-suited to this use case.

The booking write path (conflict check + booking creation) must always hit the primary database — optimistic or pessimistic locking does not work across replicas without additional coordination.

### When does it make sense to use an open-source scheduling foundation?

Cal.com's open-source scheduling stack is a viable foundation for SaaS products that need scheduling as a core feature but want to avoid building calendar integrations from scratch. It handles OAuth flows, multi-provider calendar sync, and a functional booking UI.

The trade-off is that you are inheriting someone else's data model and API shape, which constrains how much you can customize the domain logic. Evaluate it concretely: does its booking model match yours, or would you be fighting the abstraction?

For products where scheduling is a supporting feature, an open-source foundation often makes sense. For products where scheduling is the core domain, building from first principles with your own data model gives you more long-term flexibility.

Building scheduling into a SaaS product is an architecture decision worth getting right early. [Clixo works with product teams](https://clixo.sh/#contact) on exactly this kind of foundational engineering — reach out if you want a senior perspective before you commit to an approach.

---

Clixo · 1141 W Bryn Mawr Ave, Itasca, IL 60143, US · [hello@clixo.sh](mailto:hello@clixo.sh)
[Start a build](https://clixo.sh/#contact) · [All services](https://clixo.sh/services) · [Agent guide (llms.txt)](https://clixo.sh/llms.txt)
