When building an appointment scheduling system, one of the problems that looks simple at first is:
«“Find the available time slots for a doctor.”»
But the problem becomes much more interesting when different services have different durations.
For example:
- Consultation → 15 minutes
- Therapy → 45 minutes
- Follow-up → 20 minutes
- Procedure → 60 minutes
Now imagine a doctor is available from 9:00 AM to 1:00 PM, and several appointments have already been booked.
How do we find the remaining slots efficiently?
In this article, I'll explain the approach I use when designing this type of scheduling logic in Laravel.
The Basic Problem
Let's say a doctor works:
09:00 AM → 01:00 PM
And the following appointments already exist:
09:30 → 09:45 Consultation
10:15 → 11:00 Therapy
11:30 → 11:50 Follow-up
Now a patient wants to book a 45-minute therapy session.
Simply checking whether "10:00 AM" is available isn't enough.
We need to determine whether there is a continuous 45-minute window where the appointment can fit.
That distinction is extremely important.
Don't Think in Terms of Fixed Slots
One common approach is to generate slots like:
09:00
09:15
09:30
09:45
10:00
...
This works for systems where every appointment has the same duration.
But it becomes problematic when services have different durations.
Instead, I prefer thinking about the doctor's schedule as a continuous timeline.
For example:
Doctor Schedule
09:00 ───────────────────────────── 13:00
Booked:
09:30 ─ 09:45
10:15 ───────── 11:00
11:30 ───── 11:50
The gaps between appointments become the important part.
Step 1: Get the Doctor's Working Window
First, determine the doctor's availability.
For example:
$doctorStart = Carbon::parse('09:00');
$doctorEnd = Carbon::parse('13:00');
This represents the complete scheduling window.
The appointment duration comes from the selected service:
$serviceDuration = 45; // minutes
Step 2: Retrieve Existing Appointments
The next step is to retrieve all appointments that can affect the requested schedule.
For example:
$appointments = Appointment::query()
->where('doctor_id', $doctorId)
->whereDate('appointment_date', $date)
->whereIn('status', ['confirmed', 'pending'])
->orderBy('start_time')
->get();
The important thing here is that we should consider all relevant appointments, not just the first or next appointment.
If multiple appointments already exist, each one can reduce the available scheduling window.
Step 3: Find the Gaps
Suppose our working hours are:
09:00 → 13:00
And appointments are:
09:30 → 09:45
10:15 → 11:00
11:30 → 11:50
The available gaps are:
09:00 → 09:30 = 30 minutes
09:45 → 10:15 = 30 minutes
11:00 → 11:30 = 30 minutes
11:50 → 13:00 = 70 minutes
Now suppose the requested service requires 45 minutes.
Only this window can accommodate it:
11:50 → 12:35
We could also return additional possible start times inside the 70-minute window:
11:50 → 12:35
12:05 → 12:50
Depending on the business rules, we might allow 5, 10, or 15-minute increments.
Step 4: Use Interval Overlap Detection
The most important part of the implementation is detecting whether a proposed appointment overlaps an existing appointment.
Conceptually, two intervals overlap when:
New Start < Existing End
AND
New End > Existing Start
In Laravel, this logic can be expressed as:
$hasConflict = Appointment::query()
->where('doctor_id', $doctorId)
->whereDate('appointment_date', $date)
->where('start_time', '<', $newEnd)
->where('end_time', '>', $newStart)
->exists();
This is much safer than checking only whether the proposed start time already exists.
For example, this appointment:
10:00 → 10:45
must conflict with:
10:30 → 11:00
even though "10:00" isn't an existing start time.
Step 5: Generate Candidate Start Times
Once we know the free gaps, we can generate candidate start times.
For example:
$increment = 15; // minutes
For a free window:
11:50 → 13:00
and a service duration of 45 minutes:
11:50 → 12:35
12:05 → 12:50
The algorithm should stop generating candidates once:
candidateStart + serviceDuration > gapEnd
This prevents invalid slots from being returned.
A Simple Laravel Implementation
A simplified version could look like this:
$availableSlots = [];
$current = $doctorStart->copy();
foreach ($appointments as $appointment) {
$appointmentStart = Carbon::parse($appointment->start_time);
$appointmentEnd = Carbon::parse($appointment->end_time);
// Generate slots before the appointment
while (
$current->copy()->addMinutes($serviceDuration)->lte($appointmentStart)
) {
$availableSlots[] = [
'start' => $current->format('H:i'),
'end' => $current->copy()
->addMinutes($serviceDuration)
->format('H:i'),
];
$current->addMinutes($increment);
}
// Move current pointer beyond the booked appointment
if ($current->lt($appointmentEnd)) {
$current = $appointmentEnd->copy();
}
}
// Handle the remaining time after the last appointment
while (
$current->copy()->addMinutes($serviceDuration)->lte($doctorEnd)
) {
$availableSlots[] = [
'start' => $current->format('H:i'),
'end' => $current->copy()
->addMinutes($serviceDuration)
->format('H:i'),
];
$current->addMinutes($increment);
}
This is intentionally simplified. In a production system, I would separate the slot calculation from the controller and put the scheduling logic into a dedicated service class.
For example:
AppointmentController
↓
AppointmentAvailabilityService
↓
Availability calculation
↓
Appointment Repository / Model
This keeps the controller thin and makes the scheduling algorithm easier to test.
Why a Dedicated Service Class Matters
Scheduling logic tends to grow quickly.
Initially, you may only have:
Doctor availability
+
Booked appointments
Later, you may need:
Doctor breaks
+
Week offs
+
Holiday
+
Service duration
+
Buffer time
+
Room availability
+
Multiple doctors
+
Multiple locations
+
Appointment status
+
Cancellation
If all of this logic lives inside a controller, it becomes difficult to maintain.
A dedicated service makes the architecture much easier to evolve.
For example:
class AppointmentAvailabilityService
{
public function getAvailableSlots(
int $doctorId,
Carbon $date,
int $serviceDuration
): array {
// availability calculation
}
}
Now the controller only needs to ask:
$slots = $availabilityService->getAvailableSlots(
$doctorId,
$date,
$serviceDuration
);
Don't Forget Appointment Status
Another important consideration is appointment status.
For example:
confirmed
pending
cancelled
completed
no_show
A cancelled appointment normally shouldn't block a slot.
So your query should explicitly define which statuses consume availability.
For example:
->whereIn('status', [
'confirmed',
'pending',
])
Don't simply retrieve every appointment and assume every record blocks the schedule.
Database Indexing
Scheduling systems can generate a large number of availability queries.
If you frequently search appointments by:
doctor_id
appointment_date
start_time
then an appropriate composite index can make a significant difference.
For example:
$table->index([
'doctor_id',
'appointment_date',
'start_time',
]);
The exact indexes should depend on your actual query patterns and database workload.
Don't blindly add indexes everywhere.
Think About Race Conditions
There is another problem that is easy to miss.
Suppose two patients request the same slot at almost exactly the same time.
Both requests could see:
11:50 → 12:35
as available.
Both then try to book it.
This is no longer just a slot-calculation problem.
It becomes a concurrency problem.
The final booking operation should therefore re-check availability inside a transaction or use an appropriate locking/constraint strategy.
Calculating availability and actually reserving the slot should be treated as two different operations.
The Architecture I Prefer
For a larger Laravel scheduling application, I'd structure it roughly like this:
Controller
│
▼
Availability Service
│
├── Doctor Schedule
├── Service Duration
├── Existing Appointments
├── Breaks / Holidays
└── Business Rules
│
▼
Available Slots
Then the booking flow becomes:
Request Slot
↓
Calculate Availability
↓
User Selects Slot
↓
Re-check Availability
↓
Transaction
↓
Create Appointment
This separation makes the system easier to reason about and significantly reduces the chance of inconsistent bookings.
Final Thoughts
Appointment scheduling is not really a “generate some time slots” problem.
It is an interval management and constraint-solving problem.
Once you start thinking in terms of:
- Continuous availability windows
- Variable service durations
- Interval overlap
- Multiple existing appointments
- Business constraints
- Concurrency
the problem becomes much easier to design correctly.
Laravel provides everything needed to build this kind of system, but the important part is keeping the scheduling algorithm separate from your controllers and database models.
For me, the biggest lesson is:
«Don't generate slots first and try to validate them later. Model the available time first, then generate only the slots that can actually fit.»
I write more about Laravel, backend architecture, system design, and real-world application development on my portfolio: https://ajkumar.in
If you're building an appointment scheduling system with Laravel, I'd be interested to hear how you're handling variable-duration services and overlapping appointments.
Top comments (0)