Building a Scheduling System: Timestamp Management
Ever tried to schedule a meeting across three timezones and had it somehow end up at 3 AM for someone? Yeah, scheduling systems are hard. They're probably the most timezone-sensitive applications you'll ever build. But here's the good news: once you understand the patterns, you can build schedulers that actually work reliably. Let's dive into the real-world solutions that power appointment systems, booking platforms, and calendar apps.
Core Challenges in Scheduling Systems
Let me paint you a picture. You're scheduling a meeting between someone in NYC and someone in Tokyo. "2 PM tomorrow" you say. But whose 2 PM? And what happens when DST kicks in next week? These aren't edge cases—they're Tuesday.
The Fundamental Problems (That'll Keep You Up at Night)
// Scenario: User in NYC schedules meeting with user in Tokyo
// Meeting: Tomorrow at 2 PM
// Question: 2 PM in whose timezone?
// - NYC: 2024-01-16 14:00 EST (UTC-5)
// - Tokyo: 2024-01-17 03:00 JST (UTC+9) ← Next day!
// Challenge 1: Which timezone is the "source of truth"?
// Challenge 2: How to display to each participant?
// Challenge 3: What happens during DST transitions?
// Challenge 4: How to handle recurring events?
Key Requirements
- Timezone-Aware Storage: Store absolute moment (UTC) + display timezone
- User Context: Display in each user's local timezone
- DST Handling: Recurring events must respect DST transitions
- Conflict Detection: Check overlaps accounting for timezones
- Calendar Integration: Export/import with proper timezone data
Data Model Design
Event Schema
-- PostgreSQL schema for timezone-aware scheduling
CREATE TABLE events (
id SERIAL PRIMARY KEY,
title VARCHAR(255) NOT NULL,
-- Core timestamp fields
start_at TIMESTAMPTZ NOT NULL, -- Absolute moment (UTC)
end_at TIMESTAMPTZ NOT NULL,
-- Timezone context
timezone VARCHAR(50) NOT NULL, -- IANA timezone (e.g., "America/New_York")
-- Original scheduling details
scheduled_date DATE, -- Date when event was intended
scheduled_time TIME, -- Time when event was intended
-- Recurrence
is_recurring BOOLEAN DEFAULT FALSE,
recurrence_rule TEXT, -- iCalendar RRULE format
recurrence_end_date DATE,
-- Metadata
created_by_user_id INTEGER NOT NULL,
location TEXT,
description TEXT,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
CONSTRAINT valid_time_range CHECK (end_at > start_at)
);
-- Index for efficient range queries
CREATE INDEX idx_events_time_range ON events
USING GIST (tstzrange(start_at, end_at));
-- Index for user's events
CREATE INDEX idx_events_user ON events (created_by_user_id);
-- Participants (many-to-many)
CREATE TABLE event_participants (
id SERIAL PRIMARY KEY,
event_id INTEGER REFERENCES events(id) ON DELETE CASCADE,
user_id INTEGER NOT NULL,
timezone VARCHAR(50) NOT NULL, -- Participant's timezone
status VARCHAR(20) DEFAULT 'pending', -- pending, accepted, declined
UNIQUE(event_id, user_id)
);
-- User timezone preferences
CREATE TABLE users (
id SERIAL PRIMARY KEY,
email VARCHAR(255) UNIQUE NOT NULL,
default_timezone VARCHAR(50) DEFAULT 'UTC',
created_at TIMESTAMPTZ DEFAULT NOW()
);
TypeScript/JavaScript Model
// Domain models with Luxon
import { DateTime, Interval } from 'luxon';
interface Event {
id: string;
title: string;
// Store as ISO strings in database
startAt: string; // ISO 8601 with timezone: "2024-01-16T14:00:00-05:00"
endAt: string;
timezone: string; // IANA timezone
// Original scheduling intent
scheduledDate: string; // "2024-01-16"
scheduledTime: string; // "14:00:00"
isRecurring: boolean;
recurrenceRule?: string; // iCalendar RRULE
createdBy: string;
participants: Participant[];
}
interface Participant {
userId: string;
timezone: string;
status: 'pending' | 'accepted' | 'declined';
}
// Convert to DateTime objects for manipulation
function parseEvent(event: Event): {
start: DateTime;
end: DateTime;
interval: Interval;
} {
const start = DateTime.fromISO(event.startAt);
const end = DateTime.fromISO(event.endAt);
return {
start,
end,
interval: Interval.fromDateTimes(start, end),
};
}
// Display event in user's timezone
function displayEventForUser(
event: Event,
userTimezone: string
): {
localStart: string;
localEnd: string;
date: string;
timeRange: string;
} {
const { start, end } = parseEvent(event);
const localStart = start.setZone(userTimezone);
const localEnd = end.setZone(userTimezone);
return {
localStart: localStart.toISO(),
localEnd: localEnd.toISO(),
date: localStart.toFormat('MMMM d, yyyy'),
timeRange: `${localStart.toFormat('h:mm a')} - ${localEnd.toFormat('h:mm a ZZZZ')}`,
};
}
Creating Events
This is where the rubber meets the road. Creating a single event sounds simple, right? Parse the time, save it, done. Except... what about DST transitions? What about conflicts? What about that time someone tries to schedule a meeting at 2:30 AM on the day DST springs forward?
Single Event Creation (The Right Way)
import { DateTime } from 'luxon';
interface CreateEventInput {
title: string;
date: string; // "2024-01-16"
startTime: string; // "14:00"
endTime: string; // "15:00"
timezone: string; // "America/New_York"
userId: string;
}
async function createEvent(input: CreateEventInput): Promise<Event> {
// Parse user's local time
const startLocal = DateTime.fromFormat(
`${input.date} ${input.startTime}`,
'yyyy-MM-dd HH:mm',
{ zone: input.timezone }
);
const endLocal = DateTime.fromFormat(
`${input.date} ${input.endTime}`,
'yyyy-MM-dd HH:mm',
{ zone: input.timezone }
);
// Validate
if (!startLocal.isValid || !endLocal.isValid) {
throw new Error('Invalid date/time format');
}
if (endLocal <= startLocal) {
throw new Error('End time must be after start time');
}
// Check for DST edge cases
if (!startLocal.isValid) {
throw new Error('Invalid time (possibly DST transition)');
}
// Store as ISO strings (includes timezone offset)
const event: Event = {
id: generateId(),
title: input.title,
startAt: startLocal.toISO(), // "2024-01-16T14:00:00-05:00"
endAt: endLocal.toISO(),
timezone: input.timezone,
scheduledDate: input.date,
scheduledTime: input.startTime,
isRecurring: false,
createdBy: input.userId,
participants: [],
};
// Check for conflicts
const conflicts = await findConflicts(
input.userId,
startLocal,
endLocal
);
if (conflicts.length > 0) {
throw new ConflictError('Time slot already booked', conflicts);
}
// Save to database
await db.events.create(event);
return event;
}
Handling DST Transitions
import { DateTime } from 'luxon';
// Spring forward: March 10, 2024, 2:00 AM → 3:00 AM in NYC
// User tries to schedule 2:30 AM (doesn't exist)
function createEventSafeDST(
date: string,
time: string,
timezone: string
): DateTime {
const dt = DateTime.fromFormat(
`${date} ${time}`,
'yyyy-MM-dd HH:mm',
{ zone: timezone }
);
if (!dt.isValid) {
// Check if it's a DST gap
if (dt.invalidReason === 'unit out of range') {
throw new Error(
`Time ${time} does not exist on ${date} due to DST transition. ` +
`The clock jumps forward at 2:00 AM.`
);
}
throw new Error(`Invalid date/time: ${dt.invalidExplanation}`);
}
return dt;
}
// Fall back: November 3, 2024, 1:30 AM occurs twice
// Need to specify which occurrence
function createEventDuringFallback(
date: string,
time: string,
timezone: string,
isDST: boolean // User specifies which occurrence
): DateTime {
const dt = DateTime.fromFormat(
`${date} ${time}`,
'yyyy-MM-dd HH:mm',
{ zone: timezone }
);
// Luxon chooses first occurrence by default
// Adjust if user wants second occurrence
if (!isDST && dt.isInDST) {
// Want standard time, but got DST
return dt.plus({ hours: 1 });
}
return dt;
}
Recurring Events
iCalendar RRULE Standard
import { RRule, RRuleSet, rrulestr } from 'rrulejs';
import { DateTime } from 'luxon';
interface RecurrenceInput {
frequency: 'daily' | 'weekly' | 'monthly' | 'yearly';
interval: number; // Every N days/weeks/months
endDate?: string; // When to stop
count?: number; // Or after N occurrences
byWeekday?: number[]; // For weekly: [0=SU, 1=MO, ...]
byMonthDay?: number; // For monthly: day of month
}
function createRecurringEvent(
baseEvent: CreateEventInput,
recurrence: RecurrenceInput
): Event {
const { start, end } = parseEventTimes(baseEvent);
// Create RRULE
const rule = new RRule({
freq: getRRuleFrequency(recurrence.frequency),
interval: recurrence.interval,
dtstart: start.toJSDate(),
until: recurrence.endDate
? DateTime.fromISO(recurrence.endDate).toJSDate()
: undefined,
count: recurrence.count,
byweekday: recurrence.byWeekday,
bymonthday: recurrence.byMonthDay,
tzid: baseEvent.timezone, // Critical for DST handling
});
const rruleString = rule.toString();
return {
...baseEvent,
isRecurring: true,
recurrenceRule: rruleString,
recurrenceEndDate: recurrence.endDate,
};
}
// Generate occurrences for a date range
function generateOccurrences(
event: Event,
rangeStart: DateTime,
rangeEnd: DateTime
): Array<{ start: DateTime; end: DateTime }> {
if (!event.isRecurring || !event.recurrenceRule) {
return [];
}
const rule = rrulestr(event.recurrenceRule, {
dtstart: DateTime.fromISO(event.startAt).toJSDate(),
tzid: event.timezone,
});
const duration = DateTime.fromISO(event.endAt)
.diff(DateTime.fromISO(event.startAt));
const occurrences = rule.between(
rangeStart.toJSDate(),
rangeEnd.toJSDate(),
true // inclusive
);
return occurrences.map(date => {
const start = DateTime.fromJSDate(date, { zone: event.timezone });
const end = start.plus(duration);
return { start, end };
});
}
// Example: Weekly team meeting, every Monday at 10 AM
const weeklyMeeting = createRecurringEvent(
{
title: 'Weekly Team Sync',
date: '2024-01-15', // Monday
startTime: '10:00',
endTime: '11:00',
timezone: 'America/New_York',
userId: 'user123',
},
{
frequency: 'weekly',
interval: 1,
byWeekday: [1], // Monday
endDate: '2024-12-31',
}
);
// Get next 5 occurrences
const nextOccurrences = generateOccurrences(
weeklyMeeting,
DateTime.now(),
DateTime.now().plus({ months: 2 })
).slice(0, 5);
DST-Aware Recurring Events
import { DateTime } from 'luxon';
// Handle DST transitions in recurring events
function generateDSTAwareOccurrences(
baseStart: DateTime,
baseEnd: DateTime,
timezone: string,
dates: Date[] // From RRULE
): Array<{ start: DateTime; end: DateTime; dstOffset: number }> {
const duration = baseEnd.diff(baseStart);
return dates.map(date => {
// Create occurrence in original timezone
const start = DateTime.fromJSDate(date, { zone: timezone });
const end = start.plus(duration);
return {
start,
end,
dstOffset: start.offset, // Track offset changes
};
});
}
// Example spanning DST transition
const recurringMeeting = {
title: 'Weekly 9 AM Meeting',
startAt: '2024-03-01T09:00:00-05:00', // EST (UTC-5)
endAt: '2024-03-01T10:00:00-05:00',
timezone: 'America/New_York',
recurrenceRule: 'FREQ=WEEKLY;BYDAY=FR', // Every Friday
};
const occurrences = generateDSTAwareOccurrences(
DateTime.fromISO(recurringMeeting.startAt),
DateTime.fromISO(recurringMeeting.endAt),
recurringMeeting.timezone,
/* RRule generates dates */
);
// Results:
// March 1: 09:00-10:00 EST (UTC-5) ✓
// March 8: 09:00-10:00 EST (UTC-5) ✓
// March 15: 09:00-10:00 EDT (UTC-4) ✓ DST started March 10!
// Still 9 AM local time, but offset changed
Conflict Detection
Here's something that sounds simple but isn't: "Is this time slot already taken?" Turns out, detecting overlapping time intervals across timezones is surprisingly tricky. But we've got Luxon's Interval
type to save us.
Overlap Algorithm (Don't Roll Your Own)
import { Interval } from 'luxon';
async function findConflicts(
userId: string,
start: DateTime,
end: DateTime
): Promise<Event[]> {
// Query events that might overlap
const candidates = await db.events.find({
createdBy: userId,
// Efficient range query using PostgreSQL GIST index
timeRange: {
overlaps: { start: start.toISO(), end: end.toISO() }
}
});
const newInterval = Interval.fromDateTimes(start, end);
// Check for actual overlap
const conflicts = candidates.filter(event => {
const { interval } = parseEvent(event);
return interval.overlaps(newInterval);
});
return conflicts;
}
// Check conflicts across multiple participants
async function findGroupConflicts(
participantIds: string[],
start: DateTime,
end: DateTime
): Promise<Map<string, Event[]>> {
const conflicts = new Map<string, Event[]>();
await Promise.all(
participantIds.map(async (userId) => {
const userConflicts = await findConflicts(userId, start, end);
if (userConflicts.length > 0) {
conflicts.set(userId, userConflicts);
}
})
);
return conflicts;
}
Finding Available Slots
import { DateTime, Interval } from 'luxon';
interface TimeSlot {
start: DateTime;
end: DateTime;
}
async function findAvailableSlots(
userId: string,
date: string,
timezone: string,
slotDuration: number = 30, // minutes
businessHours: { start: string; end: string } = {
start: '09:00',
end: '17:00'
}
): Promise<TimeSlot[]> {
const dayStart = DateTime.fromFormat(
`${date} ${businessHours.start}`,
'yyyy-MM-dd HH:mm',
{ zone: timezone }
);
const dayEnd = DateTime.fromFormat(
`${date} ${businessHours.end}`,
'yyyy-MM-dd HH:mm',
{ zone: timezone }
);
// Get existing events for the day
const events = await db.events.find({
createdBy: userId,
startAt: { gte: dayStart.toISO() },
endAt: { lte: dayEnd.toISO() }
});
// Sort events by start time
const sortedEvents = events
.map(e => parseEvent(e))
.sort((a, b) => a.start.toMillis() - b.start.toMillis());
// Find gaps between events
const availableSlots: TimeSlot[] = [];
let currentTime = dayStart;
for (const event of sortedEvents) {
// Gap before this event?
const gapDuration = event.start.diff(currentTime, 'minutes').minutes;
if (gapDuration >= slotDuration) {
availableSlots.push({
start: currentTime,
end: event.start
});
}
// Move past this event
currentTime = event.end;
}
// Gap after last event?
const finalGap = dayEnd.diff(currentTime, 'minutes').minutes;
if (finalGap >= slotDuration) {
availableSlots.push({
start: currentTime,
end: dayEnd
});
}
return availableSlots;
}
// Find common availability across multiple users
async function findCommonAvailability(
userIds: string[],
date: string,
timezone: string,
duration: number = 60 // minutes
): Promise<TimeSlot[]> {
// Get available slots for each user
const userSlots = await Promise.all(
userIds.map(userId =>
findAvailableSlots(userId, date, timezone, duration)
)
);
// Find intersection of all availability
return findSlotIntersection(userSlots, duration);
}
function findSlotIntersection(
userSlots: TimeSlot[][],
minDuration: number
): TimeSlot[] {
if (userSlots.length === 0) return [];
// Start with first user's slots
let commonSlots = userSlots[0];
// Intersect with each subsequent user
for (let i = 1; i < userSlots.length; i++) {
commonSlots = intersectSlots(commonSlots, userSlots[i]);
}
// Filter slots that meet minimum duration
return commonSlots.filter(slot =>
slot.end.diff(slot.start, 'minutes').minutes >= minDuration
);
}
function intersectSlots(
slotsA: TimeSlot[],
slotsB: TimeSlot[]
): TimeSlot[] {
const intersections: TimeSlot[] = [];
for (const slotA of slotsA) {
for (const slotB of slotsB) {
const intervalA = Interval.fromDateTimes(slotA.start, slotA.end);
const intervalB = Interval.fromDateTimes(slotB.start, slotB.end);
const intersection = intervalA.intersection(intervalB);
if (intersection && intersection.isValid) {
intersections.push({
start: intersection.start,
end: intersection.end
});
}
}
}
return mergeAdjacentSlots(intersections);
}
Calendar Integration
iCalendar Export
import ical from 'ical-generator';
import { DateTime } from 'luxon';
function exportToICalendar(events: Event[]): string {
const calendar = ical({ name: 'My Calendar' });
for (const event of events) {
const { start, end } = parseEvent(event);
const icalEvent = calendar.createEvent({
start: start.toJSDate(),
end: end.toJSDate(),
summary: event.title,
description: event.description,
location: event.location,
timezone: event.timezone,
});
// Add recurrence rule if applicable
if (event.isRecurring && event.recurrenceRule) {
icalEvent.repeating(event.recurrenceRule);
}
// Add participants
for (const participant of event.participants) {
icalEvent.createAttendee({
email: participant.email,
status: participant.status,
});
}
}
return calendar.toString();
}
// Google Calendar format
function exportToGoogleCalendar(event: Event): string {
const { start, end } = parseEvent(event);
const params = new URLSearchParams({
action: 'TEMPLATE',
text: event.title,
dates: `${start.toFormat("yyyyMMdd'T'HHmmss")}/${end.toFormat("yyyyMMdd'T'HHmmss")}`,
ctz: event.timezone,
details: event.description || '',
location: event.location || '',
});
return `https://calendar.google.com/calendar/render?${params.toString()}`;
}
Webhook Notifications
import { DateTime } from 'luxon';
interface ReminderConfig {
eventId: string;
userId: string;
reminderMinutes: number[]; // [15, 60, 1440] = 15min, 1hr, 1day
}
async function scheduleReminders(config: ReminderConfig): Promise<void> {
const event = await db.events.findById(config.eventId);
const { start } = parseEvent(event);
for (const minutes of config.reminderMinutes) {
const reminderTime = start.minus({ minutes });
// Schedule job (using Bull, Agenda, or similar)
await queue.add(
'send-reminder',
{
userId: config.userId,
eventId: config.eventId,
event: {
title: event.title,
start: start.toISO(),
minutesUntil: minutes,
}
},
{
delay: reminderTime.diffNow().toMillis(),
attempts: 3,
}
);
}
}
// Handle reminder job
async function sendReminder(job: Job): Promise<void> {
const { userId, event } = job.data;
// Get user's current timezone
const user = await db.users.findById(userId);
const start = DateTime.fromISO(event.start).setZone(user.timezone);
// Send notification in user's local time
await notificationService.send({
userId,
title: `Upcoming: ${event.title}`,
body: `Starts at ${start.toFormat('h:mm a ZZZZ')}`,
data: { eventId: event.id },
});
}
Real-World Examples
Example 1: Doctor Appointment Booking
interface Appointment {
id: string;
patientId: string;
doctorId: string;
appointmentType: 'consultation' | 'followup' | 'checkup';
startAt: string;
endAt: string;
timezone: string;
status: 'scheduled' | 'confirmed' | 'cancelled' | 'completed';
notes?: string;
}
class AppointmentScheduler {
async bookAppointment(
patientId: string,
doctorId: string,
date: string,
timeSlot: string,
timezone: string
): Promise<Appointment> {
// 1. Check doctor availability
const doctor = await db.doctors.findById(doctorId);
const workingHours = doctor.schedule[DateTime.fromISO(date).weekday];
if (!this.isWithinWorkingHours(timeSlot, workingHours)) {
throw new Error('Doctor not available at this time');
}
// 2. Parse time in doctor's timezone (clinic timezone)
const clinicTz = doctor.clinicTimezone;
const start = DateTime.fromFormat(
`${date} ${timeSlot}`,
'yyyy-MM-dd HH:mm',
{ zone: clinicTz }
);
const duration = this.getAppointmentDuration('consultation');
const end = start.plus({ minutes: duration });
// 3. Check conflicts
const conflicts = await findConflicts(doctorId, start, end);
if (conflicts.length > 0) {
throw new ConflictError('Time slot already booked');
}
// 4. Create appointment
const appointment: Appointment = {
id: generateId(),
patientId,
doctorId,
appointmentType: 'consultation',
startAt: start.toISO(),
endAt: end.toISO(),
timezone: clinicTz, // Store in clinic timezone
status: 'scheduled',
};
await db.appointments.create(appointment);
// 5. Send confirmations
await this.sendConfirmations(appointment, timezone);
// 6. Schedule reminders
await scheduleReminders({
eventId: appointment.id,
userId: patientId,
reminderMinutes: [1440, 60, 15], // 1 day, 1 hour, 15 min
});
return appointment;
}
async sendConfirmations(
appointment: Appointment,
patientTimezone: string
): Promise<void> {
const start = DateTime.fromISO(appointment.startAt);
const patientStart = start.setZone(patientTimezone);
await emailService.send({
to: appointment.patientId,
subject: 'Appointment Confirmed',
body: `
Your appointment is confirmed for:
${patientStart.toFormat('MMMM d, yyyy')}
${patientStart.toFormat('h:mm a ZZZZ')}
Clinic timezone: ${start.toFormat('h:mm a ZZZZ')}
`
});
}
}
Example 2: Multi-Timezone Meeting Scheduler
interface Meeting {
id: string;
title: string;
organizerId: string;
participants: Participant[];
startAt: string;
endAt: string;
timezone: string; // Organizer's timezone
}
class MeetingScheduler {
async proposeMeetingTimes(
participantIds: string[],
duration: number, // minutes
dateRange: { start: string; end: string },
preferredTimezone: string
): Promise<Array<{ time: DateTime; participantViews: Map<string, string> }>> {
// 1. Get participant timezones
const participants = await db.users.find({
id: { in: participantIds }
});
// 2. Find common availability
const proposals: Array<{
time: DateTime;
participantViews: Map<string, string>;
}> = [];
let currentDate = DateTime.fromISO(dateRange.start, {
zone: preferredTimezone
});
const endDate = DateTime.fromISO(dateRange.end, {
zone: preferredTimezone
});
while (currentDate < endDate) {
const dateStr = currentDate.toISODate();
// Find common slots for this day
const commonSlots = await findCommonAvailability(
participantIds,
dateStr,
preferredTimezone,
duration
);
// Show time in each participant's timezone
for (const slot of commonSlots) {
const participantViews = new Map<string, string>();
for (const participant of participants) {
const localTime = slot.start.setZone(participant.timezone);
participantViews.set(
participant.id,
localTime.toFormat('MMM d, h:mm a ZZZZ')
);
}
proposals.push({
time: slot.start,
participantViews
});
}
currentDate = currentDate.plus({ days: 1 });
}
return proposals;
}
async scheduleMeeting(
title: string,
organizerId: string,
participantIds: string[],
selectedTime: DateTime,
duration: number
): Promise<Meeting> {
const organizer = await db.users.findById(organizerId);
const end = selectedTime.plus({ minutes: duration });
// Check all participants for conflicts
const conflicts = await findGroupConflicts(
[organizerId, ...participantIds],
selectedTime,
end
);
if (conflicts.size > 0) {
throw new ConflictError('Some participants have conflicts', conflicts);
}
// Create meeting
const meeting: Meeting = {
id: generateId(),
title,
organizerId,
participants: participantIds.map(id => ({
userId: id,
timezone: participants.find(p => p.id === id)!.timezone,
status: 'pending'
})),
startAt: selectedTime.toISO(),
endAt: end.toISO(),
timezone: organizer.timezone,
};
await db.meetings.create(meeting);
// Send invitations
await this.sendInvitations(meeting);
return meeting;
}
}
Best Practices
1. Always Store UTC + Timezone
// ✅ CORRECT
interface Event {
startAt: string; // "2024-01-16T19:00:00Z" (UTC)
endAt: string; // "2024-01-16T20:00:00Z" (UTC)
timezone: string; // "America/New_York" (for display)
}
// ❌ WRONG
interface Event {
startAt: string; // "2024-01-16T14:00:00" (no timezone!)
}
2. Validate Before DST Transitions
function validateEventTime(
date: string,
time: string,
timezone: string
): { valid: boolean; message?: string } {
const dt = DateTime.fromFormat(
`${date} ${time}`,
'yyyy-MM-dd HH:mm',
{ zone: timezone }
);
if (!dt.isValid) {
return {
valid: false,
message: `Invalid time. ${dt.invalidExplanation}. ` +
'This time may not exist due to DST transition.'
};
}
return { valid: true };
}
3. Use Interval Overlap for Conflicts
// ✅ CORRECT: Use Interval.overlaps()
const interval1 = Interval.fromDateTimes(start1, end1);
const interval2 = Interval.fromDateTimes(start2, end2);
const hasConflict = interval1.overlaps(interval2);
// ❌ WRONG: Manual comparison (error-prone)
const hasConflict = start1 < end2 && start2 < end1;
4. Display in User's Timezone
function formatForUser(event: Event, userTimezone: string): string {
const start = DateTime.fromISO(event.startAt).setZone(userTimezone);
const end = DateTime.fromISO(event.endAt).setZone(userTimezone);
return `${start.toFormat('MMM d, h:mm a')} - ${end.toFormat('h:mm a ZZZZ')}`;
}
The Bottom Line: Building Schedulers That Work
You know what's funny? I've built three different scheduling systems in my career, and every single time I thought, "This time I'll get it right from the start." Spoiler alert: I didn't. But each time, I learned what actually matters.
Here's what I wish someone had told me on day one:
The Non-Negotiables:
- UTC Storage - No exceptions. Ever. Store the absolute moment.
- Timezone Context - Keep the original timezone for display. Users care about "2 PM EST," not what that is in UTC.
- DST Handling - Don't try to be clever. Use Luxon (or your platform's equivalent) and let it handle transitions.
- Conflict Detection - Interval overlap is harder than you think. Use the
Interval
type. Trust me. - Recurring Events - iCalendar's RRULE is battle-tested. Use it.
- User Display - Show each user their local time. Always.
- Calendar Integration - People will export to Google Calendar. Plan for it.
- Testing - Test around DST transitions. Seriously. March 10 and November 3 in NYC are your best friends.
The hardest part isn't the code—it's understanding that scheduling is fundamentally about coordinating different views of the same moment in time. Get that mental model right, and the rest falls into place.
Want my real advice? Build it, test it across timezones, watch it break in interesting ways, and iterate. That's how you get good at this.
Further Reading
- Complete Guide to Unix Timestamps - Foundation concepts
- Timezone Conversion Best Practices - Core patterns
- Handling Daylight Saving Time - DST deep dive
- Database Timestamp Storage - Storage strategies
- API Design: Timestamp Formats - API patterns
Building a scheduling system? Contact us for consultation on datetime handling.