DEV Community

Nitin Doyal
Nitin Doyal

Posted on

Recurring Appointments Done Right: RRULE, Exceptions and the Four Cases That Break a Naive Rule

"Repeat every 7 days" is one line of code and about four months of support tickets.

Recurring bookings look like a scheduling feature and behave like a data-integrity feature. The moment a customer says "actually, cancel just the 14th one", you are no longer computing dates — you are maintaining a set of exceptions against a rule, and the naive model cannot express that.

Here is the shape that survives.

The four cases that break the naive rule

1. The occurrence that doesn't exist. "Every second Tuesday" or "the last Friday of the month" has no occurrence in some months. A rule engine that silently produces nothing looks fine until a customer complains that their March appointment vanished. You need an explicit answer for months with no occurrence: skip, shift to the nearest day, or hold the last one. Pick deliberately and tell the customer which one you picked.

2. The exception. A customer cancels the 14th. If you store only the rule, the 14th comes back next cycle. If you delete the rule and re-create it, you lose the history of the earlier instances. You need both: the rule, and a list of instance-level overrides.

3. Retroactive changes. Staff changes their Tuesday shift. Do the next twelve Tuesdays move? Do the past ones? Past instances should never move — they are records of what happened. Future unmodified instances move; instances with their own override stay put. This is exactly the "this / this and following / all" semantics, and it is much easier if the distinction is structural rather than a flag you retrofit.

4. Timezone and DST. "10:00 every Monday" is a wall-clock statement, not an instant. A clinic in Jodhpur doesn't shift, but a customer travelling, a remote consultation, or a DST-observing location does. Store the series' timezone, compute instants from wall-clock rules, and never store a raw UTC instant as the rule.

Model: series, rule, instances, overrides

series {
  id            uuid
  service_id    uuid
  staff_id      uuid
  timezone      text          -- IANA, e.g. 'Asia/Kolkata'
  rrule         text          -- the recurrence rule
  starts_on     date
  ends_on       date nullable
  status        active|ended
}

series_instance {
  id            uuid
  series_id     uuid
  starts_at     timestamptz   -- materialised instant
  ends_at       timestamptz
  status        scheduled|booked|cancelled|completed
  source        rule|override
}
Enter fullscreen mode Exit fullscreen mode

Two decisions do most of the work here.

Materialise instances ahead. Generate the next N occurrences (say 26 weeks) at creation time rather than computing them on read. Queries get simple, availability checks become ordinary range queries, and the exclusion constraint that prevents double-booking applies to instances exactly as it does to one-off bookings.

Override, never mutate. Cancelling one instance flips its status. Moving one instance updates that row and marks source = 'override'. The rule stays authoritative for everything else.

"This / this and following / all"

This is the interaction design that users judge you on. Implement it as an explicit operation rather than a UI-only concept:

  • This occurrence — update one instance row
  • This and following — end the current series at the previous occurrence, spawn a new series starting at this one with the new rule
  • Entire series — update the series and regenerate all unmodified future instances; leave overridden ones alone

The "this and following" case is the one people forget. It is also the one that makes an audit trail possible, because you never rewrite history — you close one series and open another.

Availability has to consider the rule, not just the slots

When a customer picks a time for a new series, you are not checking one slot. You are checking whether the whole pattern is viable: staff shifts must cover every generated instance, service duration must fit, and the resource must be free each time.

Two practical rules:

  • Validate all generated instances in one pass, and report which ones fail rather than rejecting the series wholesale. "Tuesday 10:00 is unavailable on 3 of the 26 dates — here they are" is a far better message than "invalid schedule".
  • Reserve the first instance immediately, then create the series in the same transaction. A series that exists with no reserved opening is a booking you cannot honour.

Cancelling a series

"Cancel the rest" should cancel future unscheduled instances and leave completed ones intact. If the customer has already been charged or has consumed sessions, you are in billing territory — record the cancellation against the series, not by deleting rows.

Also decide what happens to a cancelled series that is later reactivated. Restarting it as a new series is almost always simpler than resurrecting the old one, because the old one's instance set no longer matches its rule.

Testing the boring way

The tests that catch real bugs:

  • A rule with no occurrence in a given month (last Friday of February)
  • Cancel one instance, run the generator again, assert it does not come back
  • "This and following" — assert the earlier instances keep their original times
  • A DST transition inside the range — assert wall-clock time is preserved
  • Two concurrent bookings for the same instance — assert exactly one wins

If you only test the happy path, you will ship the version that works until a customer needs to cancel one date.

The summary

Recurring bookings are a small rules engine with an exception list, not a date loop. Store the rule and the timezone, materialise instances ahead, express every change as an override, and keep "this / this and following / all" as three distinct operations with different data effects.

If you would rather not maintain any of it, SWIQ handles recurring bookings, overrides and availability for Indian clinics and salons — see it on a free demo. Our guide to appointment booking software with WhatsApp covers how confirmations behave for series, and the clinic booking system guide walks through multi-provider setup.


SWIQ — your queue is calling, set up in 2 minutes

Top comments (0)