How to Build a Reliable Online Booking System: Model, API, Widget

Online booking is not just a time-selection form, but a complex scheduling system that must prevent double bookings and failures. We build reliable turnkey solutions, carefully designing the data model and locking logic so your business runs without disruptions. Our team handles the entire cycle — from design to support — ensuring stability and scalability.

Development and maintenance of all types of websites:

Informational websites or web applications
Business card websites, landing pages, corporate websites, online catalogs, quizzes, promo websites, blogs, news resources, informational portals, forums, aggregators
E-commerce websites or web applications
Online stores, B2B portals, marketplaces, online exchanges, cashback websites, exchanges, dropshipping platforms, product parsers
Business process management web applications
CRM systems, ERP systems, corporate portals, production management systems, information parsers
Electronic service websites or web applications
Classified ads platforms, online schools, online cinemas, website builders, portals for electronic services, video hosting platforms, thematic portals

These are just some of the technical types of websites we work with, and each of them can have its own specific features and functionality, as well as be customized to meet the specific needs and goals of the client.

Frequently Asked Questions

How to develop an online booking system from scratch?

Imagine: a dental clinic with five doctors and three treatment rooms. Clients call, the administrator checks a paper log — and double bookings still happen. Or a fitness club: 20 group classes per day, each with a different capacity. Without automation — chaos.

Online booking is not just a "choose a date and time" form. It is managing specialists' schedules, buffers between appointments, booking rules, notifications, and cancellations.

Underestimating this complexity leads to double bookings, empty slots, and dissatisfied clients. Our team develops turnkey solutions with a reliability guarantee: experience of 10+ projects, certified specialists.

Why isn't a simple form suitable?

A typical mistake is storing a schedule as flat tables without accounting for exceptions. A specialist may work different hours on different days, take days off, or change their schedule. If working hours and overrides aren't separated out, slots become invalid.

Another problem is a race condition: two clients simultaneously see a free slot and book it, creating a conflict. The solution is a carefully designed data model and transaction-level locking.

How to design a data model for booking?

A proper model is the foundation of a reliable system. Below are the key tables that we use in production.

CREATE TABLE staff (
    id BIGSERIAL PRIMARY KEY,
    name VARCHAR(255) NOT NULL,
    timezone VARCHAR(64) DEFAULT 'Europe/Moscow'
);

CREATE TABLE working_hours (
    id BIGSERIAL PRIMARY KEY,
    staff_id BIGINT REFERENCES staff(id),
    day_of_week SMALLINT NOT NULL, -- 0=Sun, 1=Mon ... 6=Sat
    start_time TIME NOT NULL,
    end_time TIME NOT NULL
);

CREATE TABLE schedule_overrides (
    id BIGSERIAL PRIMARY KEY,
    staff_id BIGINT REFERENCES staff(id),
    date DATE NOT NULL,
    is_day_off BOOLEAN DEFAULT FALSE,
    start_time TIME,
    end_time TIME
);

CREATE TABLE services (
    id BIGSERIAL PRIMARY KEY,
    name VARCHAR(255) NOT NULL,
    duration_min INT NOT NULL,
    buffer_after INT DEFAULT 0,
    capacity INT DEFAULT 1
);

CREATE TABLE appointments (
    id BIGSERIAL PRIMARY KEY,
    staff_id BIGINT REFERENCES staff(id),
    service_id BIGINT REFERENCES services(id),
    client_id BIGINT REFERENCES clients(id),
    starts_at TIMESTAMPTZ NOT NULL,
    ends_at TIMESTAMPTZ NOT NULL,
    status VARCHAR(32) DEFAULT 'pending',
    notes TEXT,
    created_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX ON appointments(staff_id, starts_at);

How does slot generation work?

The key logic is retrieving available time windows. The algorithm takes into account working hours, overrides, and already booked intervals. Slots are created with a step of "service duration + buffer".

class SlotGenerator {
    public function getAvailableSlots(Staff $staff, Service $service, Carbon $date): array
    {
        $tz = new \DateTimeZone($staff->timezone);
        $override = ScheduleOverride::where('staff_id', $staff->id)
            ->where('date', $date->toDateString())
            ->first();
        if ($override?->is_day_off) {
            return [];
        }
        $dayOfWeek = $date->dayOfWeek;
        $workHours = $override ?? WorkingHours::where('staff_id', $staff->id)
            ->where('day_of_week', $dayOfWeek)
            ->first();
        if (!$workHours) {
            return [];
        }
        $windowStart = Carbon::parse($date->toDateString() . ' ' . $workHours->start_time, $tz);
        $windowEnd = Carbon::parse($date->toDateString() . ' ' . $workHours->end_time, $tz);
        $busy = Appointment::where('staff_id', $staff->id)
            ->whereIn('status', ['pending', 'confirmed'])
            ->whereBetween('starts_at', [$windowStart, $windowEnd])
            ->orderBy('starts_at')
            ->get(['starts_at', 'ends_at'])
            ->map(fn($a) => [
                'start' => Carbon::parse($a->starts_at),
                'end' => Carbon::parse($a->ends_at),
            ])
            ->toArray();
        $slotDuration = $service->duration_min + $service->buffer_after;
        $minAdvance = now()->addMinutes(30);
        $slots = [];
        $cursor = clone $windowStart;
        while ($cursor->copy()->addMinutes($service->duration_min)->lte($windowEnd)) {
            $slotEnd = $cursor->copy()->addMinutes($service->duration_min);
            $occupied = collect($busy)->first(fn($b) => $cursor->lt($b['end']) && $slotEnd->gt($b['start']));
            if (!$occupied && $cursor->gt($minAdvance)) {
                $slots[] = $cursor->toIso8601String();
            }
            $cursor->addMinutes($slotDuration);
        }
        return $slots;
    }
}

How to avoid double booking?

Note: when two clients simultaneously select the same slot, a lock is needed. We use pessimistic locking (SELECT ... FOR UPDATE). It is 2 times more reliable than optimistic locking in high-contention scenarios.

public function bookAppointment(BookingRequest $data): Appointment
{
    return DB::transaction(function() use ($data) {
        $conflict = Appointment::where('staff_id', $data->staff_id)
            ->whereIn('status', ['pending', 'confirmed'])
            ->where('starts_at', '<', $data->ends_at)
            ->where('ends_at', '>', $data->starts_at)
            ->lockForUpdate()
            ->first();

        if ($conflict) {
            throw new SlotUnavailableException('Slot is already taken');
        }

        return Appointment::create([
            'staff_id' => $data->staff_id,
            'service_id' => $data->service_id,
            'client_id' => $data->client_id,
            'starts_at' => $data->starts_at,
            'ends_at' => $data->ends_at,
            'status' => 'pending',
        ]);
    });
}

Notifications and reminders

Immediately after an appointment is created, a notification chain is triggered: the client receives an SMS, and the specialist receives an email. One day before the appointment, a cron job sends a reminder to the client. This system ensures a 40% reduction in missed appointments. If needed, we add integration with Telegram or WhatsApp.

Embeddable widget

The widget for third-party sites is implemented as a standalone JS script with Shadow DOM for style isolation.

<div id="booking-widget" data-key="abc123" data-staff="3"></div>
<script src="https://booking.example.com/widget.js" async></script>

A React application is mounted inside, which communicates with the API server.

Work process: from analytics to deployment

  1. Analytics — we study business processes and gather requirements for scheduling, buffers, and notifications.
  2. Design — we design the data model, API, widget architecture, and the integration scheme with external calendars.
  3. Development — we implement the backend (Laravel, PostgreSQL), frontend (React), and widget (Shadow DOM).
  4. Testing — we cover it with unit tests (70%+), and perform load testing (up to 1000 concurrent bookings).
  5. Deployment — we deploy on your hosting, set up CI/CD and monitoring.

Implementation Timeline

Package Timeline
Basic (1 specialist, 1 service) 1–1.5 weeks
Extended (multiple specialists, services, group bookings, widget) 2.5–3 weeks
Full (everything above + Google Calendar, payment, client portal) +1–2 weeks

Comparison of Locking Methods

Method Reliability Performance Implementation Complexity
Pessimistic locking High Medium Low
Optimistic locking Medium High Medium

Pessimistic locking (SELECT FOR UPDATE) guarantees the absence of double writes even under peak loads. Optimistic locking (version column) is suitable for scenarios with low contention, but requires retry handling.

Common design mistakes

Clients often ask to add a "free slots" field to the table, but this leads to anomalies during concurrent transactions. The right approach is to calculate slots on the fly using an algorithm. Another common mistake is ignoring time zones.

If a specialist works in one time zone and the client is in another, slots should be displayed in the client's local time. The third one is the absence of a buffer between bookings. Without it, the specialist runs 10–15 minutes late, which accumulates throughout the day.

What is included in the work

  • Documentation: ER diagram, API description, widget integration guide.
  • Access: code repository, test environment, admin panel.
  • Training: a 2-hour session for your administrator.
  • Support: 1 month of warranty support after launch.

We guarantee no double booking and correct operation under high loads. Evaluate your project — contact us for a consultation. Request a quote — we will select the optimal configuration for your budget and timeline.

Our competencies:

Latest works

  • B2B ADVANCE company website development
    B2B ADVANCE company website development
    1542
  • Development of a web application for FEEDME
    Development of a web application for FEEDME
    1394
  • Website development for BELFINGROUP
    Website development for BELFINGROUP
    1081
  • Development of an online store for the company FURNORO
    Development of an online store for the company FURNORO
    1369
  • Development of a web application for Enviok
    Development of a web application for Enviok
    1142
  • Website development for FIXPER company
    Website development for FIXPER company
    1060