Booking platforms require complex availability logic and timezone handling. Proper architecture prevents double-bookings and supports various scheduling scenarios. At ZIRA Software, we've built booking systems serving thousands of appointments daily.
Database Schema
// Providers (doctors, consultants, venues, etc.)
Schema::create('providers', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained();
$table->string('name');
$table->string('timezone')->default('UTC');
$table->integer('slot_duration')->default(30); // minutes
$table->integer('buffer_time')->default(0); // between appointments
$table->timestamps();
});
// Weekly availability schedule
Schema::create('availability_schedules', function (Blueprint $table) {
$table->id();
$table->foreignId('provider_id')->constrained()->onDelete('cascade');
$table->tinyInteger('day_of_week'); // 0-6 (Sunday-Saturday)
$table->time('start_time');
$table->time('end_time');
$table->boolean('is_available')->default(true);
$table->timestamps();
$table->unique(['provider_id', 'day_of_week']);
});
// Blocked dates (holidays, vacations)
Schema::create('blocked_dates', function (Blueprint $table) {
$table->id();
$table->foreignId('provider_id')->constrained()->onDelete('cascade');
$table->date('date');
$table->string('reason')->nullable();
$table->timestamps();
$table->unique(['provider_id', 'date']);
});
// Bookings
Schema::create('bookings', function (Blueprint $table) {
$table->id();
$table->foreignId('provider_id')->constrained();
$table->foreignId('customer_id')->constrained('users');
$table->foreignId('service_id')->constrained();
$table->dateTime('starts_at');
$table->dateTime('ends_at');
$table->string('status')->default('pending');
$table->text('notes')->nullable();
$table->timestamps();
$table->index(['provider_id', 'starts_at', 'ends_at']);
});
Availability Service
// app/Services/AvailabilityService.php
class AvailabilityService
{
public function getAvailableSlots(Provider $provider, Carbon $date): Collection
{
// Check if date is blocked
if ($this->isDateBlocked($provider, $date)) {
return collect();
}
// Get schedule for this day of week
$schedule = $provider->schedules()
->where('day_of_week', $date->dayOfWeek)
->where('is_available', true)
->first();
if (!$schedule) {
return collect();
}
// Generate time slots
$slots = $this->generateSlots(
$date,
$schedule->start_time,
$schedule->end_time,
$provider->slot_duration,
$provider->buffer_time
);
// Filter out booked slots
$bookedSlots = $this->getBookedSlots($provider, $date);
return $slots->reject(function ($slot) use ($bookedSlots) {
return $this->isSlotBooked($slot, $bookedSlots);
});
}
private function generateSlots(
Carbon $date,
string $startTime,
string $endTime,
int $duration,
int $buffer
): Collection {
$slots = collect();
$current = $date->copy()->setTimeFromTimeString($startTime);
$end = $date->copy()->setTimeFromTimeString($endTime);
while ($current->copy()->addMinutes($duration)->lte($end)) {
$slots->push([
'start' => $current->copy(),
'end' => $current->copy()->addMinutes($duration),
]);
$current->addMinutes($duration + $buffer);
}
return $slots;
}
private function isSlotBooked(array $slot, Collection $bookedSlots): bool
{
return $bookedSlots->contains(function ($booked) use ($slot) {
return $slot['start']->lt($booked['ends_at']) &&
$slot['end']->gt($booked['starts_at']);
});
}
}
Booking Controller
// app/Http/Controllers/BookingController.php
class BookingController extends Controller
{
public function __construct(
private AvailabilityService $availability,
private BookingService $bookingService,
) {}
public function getAvailability(Provider $provider, Request $request)
{
$request->validate([
'date' => 'required|date|after_or_equal:today',
]);
$date = Carbon::parse($request->date);
$slots = $this->availability->getAvailableSlots($provider, $date);
// Convert to customer's timezone
$customerTimezone = $request->get('timezone', 'UTC');
return response()->json([
'slots' => $slots->map(fn ($slot) => [
'start' => $slot['start']->setTimezone($customerTimezone)->toISOString(),
'end' => $slot['end']->setTimezone($customerTimezone)->toISOString(),
]),
]);
}
public function store(Request $request)
{
$validated = $request->validate([
'provider_id' => 'required|exists:providers,id',
'service_id' => 'required|exists:services,id',
'starts_at' => 'required|date|after:now',
'timezone' => 'required|timezone',
]);
$provider = Provider::findOrFail($validated['provider_id']);
$startsAt = Carbon::parse($validated['starts_at'], $validated['timezone'])
->setTimezone($provider->timezone);
// Prevent race conditions with locking
return DB::transaction(function () use ($provider, $validated, $startsAt) {
// Lock provider's bookings for this time
$conflicting = Booking::where('provider_id', $provider->id)
->where('status', '!=', 'cancelled')
->where('starts_at', '<', $startsAt->copy()->addMinutes($provider->slot_duration))
->where('ends_at', '>', $startsAt)
->lockForUpdate()
->exists();
if ($conflicting) {
return response()->json([
'message' => 'This time slot is no longer available.',
], 409);
}
$booking = Booking::create([
'provider_id' => $provider->id,
'customer_id' => auth()->id(),
'service_id' => $validated['service_id'],
'starts_at' => $startsAt,
'ends_at' => $startsAt->copy()->addMinutes($provider->slot_duration),
'status' => 'confirmed',
]);
event(new BookingCreated($booking));
return new BookingResource($booking);
});
}
}
Calendar Frontend
// resources/js/components/BookingCalendar.vue
import FullCalendar from '@fullcalendar/vue3';
import dayGridPlugin from '@fullcalendar/daygrid';
import interactionPlugin from '@fullcalendar/interaction';
export default {
components: { FullCalendar },
data() {
return {
selectedDate: null,
availableSlots: [],
calendarOptions: {
plugins: [dayGridPlugin, interactionPlugin],
initialView: 'dayGridMonth',
selectable: true,
dateClick: this.handleDateClick,
validRange: {
start: new Date(),
},
},
};
},
methods: {
async handleDateClick(info) {
this.selectedDate = info.dateStr;
await this.fetchAvailableSlots();
},
async fetchAvailableSlots() {
const response = await fetch(
`/api/providers/${this.providerId}/availability?` +
`date=${this.selectedDate}&timezone=${this.userTimezone}`
);
const data = await response.json();
this.availableSlots = data.slots;
},
async bookSlot(slot) {
await fetch('/api/bookings', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
provider_id: this.providerId,
service_id: this.serviceId,
starts_at: slot.start,
timezone: this.userTimezone,
}),
});
},
},
};
Conclusion
Booking platforms require careful handling of availability, timezones, and conflict prevention. Proper database design and locking strategies ensure reliable scheduling.
Building a booking platform? Contact ZIRA Software for scheduling system development.