Prompt and context
Design event conflict handling for a Google Calendar-like product. Users create, update, and delete events with attendees, start and end times, time zones, recurrence rules, reminders, visibility, and meeting links. A create or update must detect conflicts and support reject, warn-and-proceed, or override policies. Assume free/busy reads greatly outnumber writes, common checks need tens of milliseconds, and private calendars expose only busy state.
What the interviewer evaluates
A strong answer defines conflict semantics before naming a database. The signals are a half-open interval rule that avoids false conflicts at boundaries; a canonical event model separated from an overlap index; explicit RRULE, exception, and materialization limits; conversion from local time rules to absolute instants; an outbox or CDC path for the derived index; and a concurrency story for two writers reserving the same resource. “Query the database for overlaps” alone leaves these failure modes unanswered.
Clarifying questions
- Do adjacent intervals conflict? If not, use the half-open interval
[start, end). - How far must a recurring series be expanded? An unbounded series needs a rolling window or query-time expansion.
- Does any busy attendee block the event, or only required attendees or the organizer? This sets query fan-out and policy.
- Is double booking rejected, warned, or allowed? A conference room and a personal calendar may use different policies.
- Should another user see full details, a busy interval, or nothing? This determines ACLs and response shape.
A 30-second answer
“I keep canonical event and attendee records, plus a calendar-partitioned free-busy index. A conflict exists only when existing.start < new.end and new.start < existing.end; free events do not occupy time. I store recurrence rules in the event time zone, materialize a bounded horizon, and record exceptions. Version checks or resource locks protect writes, and an outbox updates the derived index after commit. Responses expose only ACL-allowed busy information, while monitoring covers index drift, rebuilds, and notification delay.”
Step-by-step solution
1. Define time and the conflict predicate
Represent each occurrence as [startutc, endutc), while retaining the original time zone and local rule for display and expansion. Two occurrences conflict exactly when a.start < b.end and b.start < a.end. Thus 10:00–11:00 and 11:00–12:00 are adjacent, not overlapping; reject zero-length or reversed intervals. Convert all-day events to boundaries in the calendar time zone before applying the same predicate.
2. Separate the canonical model from the availability index
The canonical layer stores events, attendees, RRULEs, exceptions, ACLs, and versions for editing and audit. The availability index stores only calendar_id, occurrence bounds, occupancy state, event ID, and a visibility label, sorted by calendar and start time. Conflict queries scan the requested window instead of large event JSON. Partition by month or tenant; hot calendars may need a dedicated shard or cache.
3. Handle recurrence and exceptions
Store iCalendar-style RRULEs and expand occurrences for a requested horizon. A rolling materialization can generate the next 90 days and extend as the boundary approaches; farther queries expand on demand and cache the result. this occurrence, this and future, and whole-series edits create exception records or a new series version rather than rewriting past occurrences. Each occurrence participates independently in conflict checks and notifications.
4. Write path and concurrency control
Validate time, ACLs, and attendee policy, then check the current index version in the same transaction. Personal calendars can use optimistic versions and ask clients to retry; strictly exclusive rooms or appointment slots can use per-resource locks or database exclusion constraints. Commit the event and an outbox record together; consumers update the index and send notifications. If the index lags, read from the canonical store or mark the answer uncertain—never silently claim no conflict.
5. Privacy, queries, and scale
Layer responses by ACL: authorized viewers see titles and attendees, while others see only busy intervals and private events appear as opaque blocks. Map free, tentative, and out-of-office states to occupancy or warnings according to policy. Cache common windows for read-heavy traffic; include a calendar version or invalidation watermark in the key. Cross-organization free/busy calls need timeouts and partial-result semantics so one external calendar cannot stall every attendee.
6. Consistency, repair, and failure paths
The canonical event is the source of truth and the index is rebuildable. An outbox or CDC guarantees that a successful commit eventually produces an index update; consumers process versions idempotently so an old message cannot overwrite a newer one. Periodically compare canonical occurrences with index hashes or counts, rebuild a calendar when they drift, and record the affected range. Retry failed notifications without rolling back an event that already committed.
High-quality sample answer
I would first define conflicts with [start, end): only strict intersection conflicts, adjacent events do not, and free events consume no availability. The canonical event model stores the original time zone, RRULE, exceptions, attendees, and ACLs. A separate calendar-partitioned occurrence index stores only bounds, occupancy, and event ID for fast window scans. RRULEs stay in the event time zone; I materialize the next 90 days, expand farther ranges on demand, and represent single-occurrence, future-occurrence, and whole-series edits as explicit exceptions or versions.
The write path validates permissions and policy, then uses an optimistic version or resource lock for concurrent edits. The event and outbox record commit together; version-aware consumers update the index idempotently and deliver notifications. The index is derived and can be rebuilt from canonical data, and conflict responses are ACL-filtered. I would warn rather than block double booking on a personal calendar, but use an exclusion constraint or lock for a room. I would monitor index drift, stale versions, query latency, and external free/busy timeouts.
Common mistakes
- Mistake: using
start <= other.endfor overlap → adjacent events become false conflicts → state the half-open interval rule and reject invalid zero-length events. - Mistake: expanding an unbounded recurring series on every request → latency and work have no upper bound → use a rolling materialization horizon and on-demand expansion beyond it.
- Mistake: using full event JSON as the conflict index → reads scan large fields and leak private details → maintain a time-and-occupancy projection and redact by ACL.
- Mistake: rolling back the event when index update fails → the source of truth becomes coupled to search and notifications → commit an outbox, retry asynchronously, and provide rebuilds.
- Mistake: taking a global lock for every calendar → hot calendars slow the entire service → lock only strictly exclusive resources and use version checks for ordinary calendars.
Follow-ups and responses
How would you find a time when every attendee is free?
Query each required attendee’s busy intervals for the target window, merge intervals on a common timeline, and take the complement of their union. Fan-out grows with attendee count, so run queries in parallel, cap the window, and return partial or uncertain results for unauthorized or timed-out calendars rather than exposing private details.
A recurring meeting conflicts only on some dates after a DST change. How do you debug it?
Inspect the RRULE time-zone identifier, original local time, expansion-library version, and exception records. Do not convert local time to UTC and discard the zone before expanding. Replay a fixture that crosses the DST boundary and compare each occurrence’s local display time, UTC bounds, and overlap predicate to separate expansion bugs from index materialization bugs.
What changes when a room must never be double-booked?
Two requests can both observe the room as free. Protect the resource time range with a database exclusion constraint, serializable transaction, or short resource lock, and return the conflicting occurrence on failure. The lock covers only the commit window, never a cache; shard hot resources and cap retries so one lock queue cannot starve other calendars.