Development of a Video Consultation Booking System
A simple booking form doesn't account for a specialist's real availability. Double bookings, time zone confusion, and client forgetfulness are typical problems. According to statistics, up to 30% of appointments are missed due to lack of reminders, and under peak load (1000+ requests per minute) the conflict probability rises to 15%. For example, for one network of medical centers we built a system handling up to 500 requests per second with zero conflict rate. We solve these problems at the architecture level: pessimistic locking, automatic time zone conversion, and multi-layered reminders. This article covers how to build a reliable video consultation booking system synced with Google Calendar, with notifications and conflict protection.
Problems We Solve
The first is request race: two clients pick the same slot simultaneously. Without locks, both receive confirmation. We use pessimistic locking at the PostgreSQL level, eliminating race conditions. PostgreSQL guarantees transaction atomicity with FOR UPDATE NOWAIT.
Second: time zones. A client in Moscow, a specialist in New York. The system automatically converts time via timestamptz and user time zone. No manual calculation.
Third: reminders. Without them, up to 30% of appointments are missed. We configure email and push notifications 24 hours and 1 hour before the consultation.
Fourth: cancellation or rescheduling. Without built-in mechanisms, calendar sync and refunds become problematic. Our API supports the full booking lifecycle.
How to Prevent Double Booking?
At the core is a transaction with row locking FOR UPDATE NOWAIT. When booking, we check for interval overlap in the bookings table. If the slot is taken, we return 409 Conflict.
app.post('/api/bookings', authenticate, async (req, res) => {
const { specialistId, startsAt, durationMinutes } = req.body;
const endsAt = addMinutes(new Date(startsAt), durationMinutes);
try {
const booking = await db.transaction(async (trx) => {
const conflict = await trx.query(
`SELECT id FROM bookings
WHERE specialist_id = $1
AND status = 'confirmed'
AND tstzrange(starts_at, ends_at) && tstzrange($2::timestamptz, $3::timestamptz)
FOR UPDATE NOWAIT`,
[specialistId, startsAt, endsAt.toISOString()]
);
if (conflict.rows.length > 0) {
throw Object.assign(new Error('Slot taken'), { code: 'CONFLICT' });
}
const [booking] = await trx.query(
`INSERT INTO bookings (specialist_id, client_id, starts_at, ends_at)
VALUES ($1, $2, $3, $4) RETURNING *`,
[specialistId, req.user.id, startsAt, endsAt.toISOString()]
);
return booking;
});
await syncToGoogleCalendar(booking);
await sendBookingConfirmation(booking, req.user);
await notifySpecialist(booking, req.user);
await scheduleReminders(booking);
res.json(booking);
} catch (err: any) {
if (err.code === 'CONFLICT') {
return res.status(409).json({ error: 'Slot is no longer available' });
}
throw err;
}
});
How to Sync Schedule with Google Calendar?
We use OAuth2 and the official googleapis client. After creating a booking, we automatically create an event in the specialist's calendar with a conference link. On cancellation, the event is deleted. This saves time and eliminates errors.
import { google } from 'googleapis';
async function syncToGoogleCalendar(booking: Booking) {
const specialist = await db.specialists.findById(booking.specialist_id);
if (!specialist.google_calendar_token) return;
const oauth2Client = new google.auth.OAuth2(
process.env.GOOGLE_CLIENT_ID,
process.env.GOOGLE_CLIENT_SECRET
);
oauth2Client.setCredentials(specialist.google_calendar_token);
const calendar = google.calendar({ version: 'v3', auth: oauth2Client });
const client = await db.users.findById(booking.client_id);
const event = await calendar.events.insert({
calendarId: 'primary',
requestBody: {
summary: `Консультация с ${client.name}`,
start: { dateTime: booking.starts_at.toISOString() },
end: { dateTime: booking.ends_at.toISOString() },
attendees: [{ email: client.email }],
conferenceData: {
createRequest: { requestId: booking.id },
},
},
conferenceDataVersion: 1,
});
await db.bookings.update(booking.id, { google_event_id: event.data.id });
}
Data Structure and Slot Search Algorithm
We store standard schedules, exceptions (vacations, holidays), and bookings. The getAvailableSlots algorithm first selects the standard hours for the weekday, checks overrides, then subtracts occupied intervals.
CREATE TABLE availability_schedules (
id UUID PRIMARY KEY,
specialist_id UUID REFERENCES specialists(id),
day_of_week SMALLINT NOT NULL, -- 1=Mon ... 7=Sun
start_time TIME NOT NULL,
end_time TIME NOT NULL,
is_active BOOLEAN DEFAULT true
);
CREATE TABLE availability_overrides (
id UUID PRIMARY KEY,
specialist_id UUID REFERENCES specialists(id),
date DATE NOT NULL,
type VARCHAR(50), -- 'blocked' | 'custom_hours'
start_time TIME,
end_time TIME
);
CREATE TABLE bookings (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
specialist_id UUID REFERENCES specialists(id),
client_id UUID REFERENCES users(id),
starts_at TIMESTAMPTZ NOT NULL,
ends_at TIMESTAMPTZ NOT NULL,
status VARCHAR(50) DEFAULT 'confirmed',
cancel_reason TEXT,
google_event_id VARCHAR(255),
created_at TIMESTAMPTZ DEFAULT now()
);
async function getAvailableSlots(
specialistId: string,
date: string,
durationMinutes: number,
userTimezone: string
): Promise<Array<{ start: string; end: string }>> {
const localDate = new Date(`${date}T00:00:00`);
const dayOfWeek = getISODayOfWeek(localDate);
const schedule = await db.query<{ start_time: string; end_time: string }>(
`SELECT start_time, end_time FROM availability_schedules
WHERE specialist_id = $1 AND day_of_week = $2 AND is_active = true`,
[specialistId, dayOfWeek]
);
if (!schedule.rows.length) return [];
const override = await db.query(
`SELECT * FROM availability_overrides
WHERE specialist_id = $1 AND date = $2`,
[specialistId, date]
);
if (override.rows[0]?.type === 'blocked') return [];
const workStart = override.rows[0]?.start_time ?? schedule.rows[0].start_time;
const workEnd = override.rows[0]?.end_time ?? schedule.rows[0].end_time;
const booked = await db.query<{ starts_at: string; ends_at: string }>(
`SELECT starts_at, ends_at FROM bookings
WHERE specialist_id = $1
AND DATE(starts_at AT TIME ZONE $3) = $2
AND status = 'confirmed'`,
[specialistId, date, userTimezone]
);
const slots: Array<{ start: string; end: string }> = [];
let current = parseTime(date, workStart, userTimezone);
const end = parseTime(date, workEnd, userTimezone);
while (current < end) {
const slotEnd = addMinutes(current, durationMinutes);
if (slotEnd > end) break;
const isBusy = booked.rows.some(b =>
current < new Date(b.ends_at) && slotEnd > new Date(b.starts_at)
);
if (!isBusy) {
slots.push({
start: current.toISOString(),
end: slotEnd.toISOString(),
});
}
current = addMinutes(current, durationMinutes);
}
return slots;
}
Time Selection Component in React
The user selects a date, and the system loads available slots. The interface is responsive and works on mobile.
function BookingCalendar({ specialistId, durationMinutes }) {
const [selectedDate, setSelectedDate] = useState<Date | null>(null);
const [slots, setSlots] = useState<Slot[]>([]);
const [selectedSlot, setSelectedSlot] = useState<Slot | null>(null);
useEffect(() => {
if (!selectedDate) return;
fetch(`/api/specialists/${specialistId}/slots?date=${formatDate(selectedDate)}&duration=${durationMinutes}`)
.then(r => r.json())
.then(setSlots);
}, [selectedDate]);
return (
<div className="grid grid-cols-2 gap-8">
<CalendarPicker
value={selectedDate}
onChange={setSelectedDate}
minDate={new Date()}
maxDate={addDays(new Date(), 60)}
disabledDates={/* выходные и блокированные дни */}
/>
{selectedDate && (
<div>
<p className="font-semibold mb-3">{formatDate(selectedDate, 'd MMMM')}</p>
{slots.length === 0 ? (
<p className="text-gray-500">Нет доступных слотов</p>
) : (
<div className="grid grid-cols-3 gap-2">
{slots.map(slot => (
<button
key={slot.start}
onClick={() => setSelectedSlot(slot)}
className={`py-2 text-sm rounded-lg border transition ${
selectedSlot?.start === slot.start
? 'border-blue-600 bg-blue-50 text-blue-700'
: 'border-gray-200 hover:border-blue-400'
}`}
>
{formatTime(slot.start)}
</button>
))}
</div>
)}
{selectedSlot && (
<button onClick={confirmBooking} className="mt-4 btn-primary w-full">
Записаться на {formatTime(selectedSlot.start)}
</button>
)}
</div>
)}
</div>
);
}
Comparison: Custom Solution vs Ready Services
| Criteria | Our Solution | Calendly / YouCanBookMe |
|---|---|---|
| Customization flexibility | Full customization to match site UI | Limited by templates |
| Integration with 1C/CRM | Via API | No |
| Cost for a team | Individual estimate | Monthly subscription (per user) |
| Data control | Storage on own servers | Data on service side |
| Source code | Full access | Closed |
A custom solution delivers up to 2x performance gain under peak load due to no overhead from external APIs.
When to Order Custom Booking?
Ready services are suitable for quick launch, but if you need a unique interface, 1C integration, or full data control, custom development is better. It pays off by eliminating monthly fees and allowing the system to scale with business growth. Get a consultation from our engineer — they will help choose the optimal path.
Process and Timelines
| Stage | Duration | Result |
|---|---|---|
| Analysis | 1-2 days | Data schema, stack selection, integration prototype |
| API development | 3-5 days | REST endpoints for booking, slots, calendar |
| Frontend | 3-5 days | Calendar, booking form, consultation page |
| Calendar integration | 1-2 days | Google Calendar, Outlook, sync |
| Testing | 1-2 days | Load (1000+ requests), unit, UI |
| Deployment | 1 day | Docker, CI/CD, environment setup |
Estimated timeline: 1.5 to 3 weeks depending on integration complexity. Contact us for an accurate project estimate.
Multi-calendar sync setup
Besides Google Calendar, we support Outlook Calendar and any CalDAV-compatible services. Each calendar gets its own OAuth session. In the code above, simply replace the provider and update the token. A custom adapter can be added if needed.What's Included in the Result
- Source code in TypeScript and React
- REST API with documentation (Swagger)
- Deployment and maintenance instructions
- 30-day bug-free guarantee
- Customization options for business processes
Order a booking system development with quality guarantee. Contact us for a project estimate — we'll calculate budget and timeline. Over 7 years of experience and 15+ implemented booking systems ensure a reliable solution.







