DEV Community

Ajay Kumar
Ajay Kumar

Posted on

How I Approach Appointment Slot Optimization in Laravel When Services Have Different Durations

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();
}
Enter fullscreen mode Exit fullscreen mode

}

// 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);
Enter fullscreen mode Exit fullscreen mode

}

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)