<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Ajay Kumar</title>
    <description>The latest articles on DEV Community by Ajay Kumar (@hiajayy).</description>
    <link>https://dev.to/hiajayy</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4089113%2F94581a10-956b-4a4a-994a-02b65d46c9e6.png</url>
      <title>DEV Community: Ajay Kumar</title>
      <link>https://dev.to/hiajayy</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/hiajayy"/>
    <language>en</language>
    <item>
      <title>How I Approach Appointment Slot Optimization in Laravel When Services Have Different Durations</title>
      <dc:creator>Ajay Kumar</dc:creator>
      <pubDate>Sat, 22 Aug 2026 03:54:13 +0000</pubDate>
      <link>https://dev.to/hiajayy/how-i-approach-appointment-slot-optimization-in-laravel-when-services-have-different-durations-5f27</link>
      <guid>https://dev.to/hiajayy/how-i-approach-appointment-slot-optimization-in-laravel-when-services-have-different-durations-5f27</guid>
      <description>&lt;p&gt;When building an appointment scheduling system, one of the problems that looks simple at first is:&lt;/p&gt;

&lt;p&gt;«“Find the available time slots for a doctor.”»&lt;/p&gt;

&lt;p&gt;But the problem becomes much more interesting when different services have different durations.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Consultation → 15 minutes&lt;/li&gt;
&lt;li&gt;Therapy → 45 minutes&lt;/li&gt;
&lt;li&gt;Follow-up → 20 minutes&lt;/li&gt;
&lt;li&gt;Procedure → 60 minutes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now imagine a doctor is available from 9:00 AM to 1:00 PM, and several appointments have already been booked.&lt;/p&gt;

&lt;p&gt;How do we find the remaining slots efficiently?&lt;/p&gt;

&lt;p&gt;In this article, I'll explain the approach I use when designing this type of scheduling logic in Laravel.&lt;/p&gt;

&lt;p&gt;The Basic Problem&lt;/p&gt;

&lt;p&gt;Let's say a doctor works:&lt;/p&gt;

&lt;p&gt;09:00 AM → 01:00 PM&lt;/p&gt;

&lt;p&gt;And the following appointments already exist:&lt;/p&gt;

&lt;p&gt;09:30 → 09:45  Consultation&lt;br&gt;
10:15 → 11:00  Therapy&lt;br&gt;
11:30 → 11:50  Follow-up&lt;/p&gt;

&lt;p&gt;Now a patient wants to book a 45-minute therapy session.&lt;/p&gt;

&lt;p&gt;Simply checking whether "10:00 AM" is available isn't enough.&lt;/p&gt;

&lt;p&gt;We need to determine whether there is a continuous 45-minute window where the appointment can fit.&lt;/p&gt;

&lt;p&gt;That distinction is extremely important.&lt;/p&gt;

&lt;p&gt;Don't Think in Terms of Fixed Slots&lt;/p&gt;

&lt;p&gt;One common approach is to generate slots like:&lt;/p&gt;

&lt;p&gt;09:00&lt;br&gt;
09:15&lt;br&gt;
09:30&lt;br&gt;
09:45&lt;br&gt;
10:00&lt;br&gt;
...&lt;/p&gt;

&lt;p&gt;This works for systems where every appointment has the same duration.&lt;/p&gt;

&lt;p&gt;But it becomes problematic when services have different durations.&lt;/p&gt;

&lt;p&gt;Instead, I prefer thinking about the doctor's schedule as a continuous timeline.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;p&gt;Doctor Schedule&lt;br&gt;
09:00 ───────────────────────────── 13:00&lt;/p&gt;

&lt;p&gt;Booked:&lt;br&gt;
       09:30 ─ 09:45&lt;br&gt;
                10:15 ───────── 11:00&lt;br&gt;
                              11:30 ───── 11:50&lt;/p&gt;

&lt;p&gt;The gaps between appointments become the important part.&lt;/p&gt;

&lt;p&gt;Step 1: Get the Doctor's Working Window&lt;/p&gt;

&lt;p&gt;First, determine the doctor's availability.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;p&gt;$doctorStart = Carbon::parse('09:00');&lt;br&gt;
$doctorEnd   = Carbon::parse('13:00');&lt;/p&gt;

&lt;p&gt;This represents the complete scheduling window.&lt;/p&gt;

&lt;p&gt;The appointment duration comes from the selected service:&lt;/p&gt;

&lt;p&gt;$serviceDuration = 45; // minutes&lt;/p&gt;

&lt;p&gt;Step 2: Retrieve Existing Appointments&lt;/p&gt;

&lt;p&gt;The next step is to retrieve all appointments that can affect the requested schedule.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;p&gt;$appointments = Appointment::query()&lt;br&gt;
    -&amp;gt;where('doctor_id', $doctorId)&lt;br&gt;
    -&amp;gt;whereDate('appointment_date', $date)&lt;br&gt;
    -&amp;gt;whereIn('status', ['confirmed', 'pending'])&lt;br&gt;
    -&amp;gt;orderBy('start_time')&lt;br&gt;
    -&amp;gt;get();&lt;/p&gt;

&lt;p&gt;The important thing here is that we should consider all relevant appointments, not just the first or next appointment.&lt;/p&gt;

&lt;p&gt;If multiple appointments already exist, each one can reduce the available scheduling window.&lt;/p&gt;

&lt;p&gt;Step 3: Find the Gaps&lt;/p&gt;

&lt;p&gt;Suppose our working hours are:&lt;/p&gt;

&lt;p&gt;09:00 → 13:00&lt;/p&gt;

&lt;p&gt;And appointments are:&lt;/p&gt;

&lt;p&gt;09:30 → 09:45&lt;br&gt;
10:15 → 11:00&lt;br&gt;
11:30 → 11:50&lt;/p&gt;

&lt;p&gt;The available gaps are:&lt;/p&gt;

&lt;p&gt;09:00 → 09:30   = 30 minutes&lt;/p&gt;

&lt;p&gt;09:45 → 10:15   = 30 minutes&lt;/p&gt;

&lt;p&gt;11:00 → 11:30   = 30 minutes&lt;/p&gt;

&lt;p&gt;11:50 → 13:00   = 70 minutes&lt;/p&gt;

&lt;p&gt;Now suppose the requested service requires 45 minutes.&lt;/p&gt;

&lt;p&gt;Only this window can accommodate it:&lt;/p&gt;

&lt;p&gt;11:50 → 12:35&lt;/p&gt;

&lt;p&gt;We could also return additional possible start times inside the 70-minute window:&lt;/p&gt;

&lt;p&gt;11:50 → 12:35&lt;br&gt;
12:05 → 12:50&lt;/p&gt;

&lt;p&gt;Depending on the business rules, we might allow 5, 10, or 15-minute increments.&lt;/p&gt;

&lt;p&gt;Step 4: Use Interval Overlap Detection&lt;/p&gt;

&lt;p&gt;The most important part of the implementation is detecting whether a proposed appointment overlaps an existing appointment.&lt;/p&gt;

&lt;p&gt;Conceptually, two intervals overlap when:&lt;/p&gt;

&lt;p&gt;New Start &amp;lt; Existing End&lt;br&gt;
AND&lt;br&gt;
New End &amp;gt; Existing Start&lt;/p&gt;

&lt;p&gt;In Laravel, this logic can be expressed as:&lt;/p&gt;

&lt;p&gt;$hasConflict = Appointment::query()&lt;br&gt;
    -&amp;gt;where('doctor_id', $doctorId)&lt;br&gt;
    -&amp;gt;whereDate('appointment_date', $date)&lt;br&gt;
    -&amp;gt;where('start_time', '&amp;lt;', $newEnd)&lt;br&gt;
    -&amp;gt;where('end_time', '&amp;gt;', $newStart)&lt;br&gt;
    -&amp;gt;exists();&lt;/p&gt;

&lt;p&gt;This is much safer than checking only whether the proposed start time already exists.&lt;/p&gt;

&lt;p&gt;For example, this appointment:&lt;/p&gt;

&lt;p&gt;10:00 → 10:45&lt;/p&gt;

&lt;p&gt;must conflict with:&lt;/p&gt;

&lt;p&gt;10:30 → 11:00&lt;/p&gt;

&lt;p&gt;even though "10:00" isn't an existing start time.&lt;/p&gt;

&lt;p&gt;Step 5: Generate Candidate Start Times&lt;/p&gt;

&lt;p&gt;Once we know the free gaps, we can generate candidate start times.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;p&gt;$increment = 15; // minutes&lt;/p&gt;

&lt;p&gt;For a free window:&lt;/p&gt;

&lt;p&gt;11:50 → 13:00&lt;/p&gt;

&lt;p&gt;and a service duration of 45 minutes:&lt;/p&gt;

&lt;p&gt;11:50 → 12:35&lt;br&gt;
12:05 → 12:50&lt;/p&gt;

&lt;p&gt;The algorithm should stop generating candidates once:&lt;/p&gt;

&lt;p&gt;candidateStart + serviceDuration &amp;gt; gapEnd&lt;/p&gt;

&lt;p&gt;This prevents invalid slots from being returned.&lt;/p&gt;

&lt;p&gt;A Simple Laravel Implementation&lt;/p&gt;

&lt;p&gt;A simplified version could look like this:&lt;/p&gt;

&lt;p&gt;$availableSlots = [];&lt;/p&gt;

&lt;p&gt;$current = $doctorStart-&amp;gt;copy();&lt;/p&gt;

&lt;p&gt;foreach ($appointments as $appointment) {&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$appointmentStart = Carbon::parse($appointment-&amp;gt;start_time);
$appointmentEnd   = Carbon::parse($appointment-&amp;gt;end_time);

// Generate slots before the appointment
while (
    $current-&amp;gt;copy()-&amp;gt;addMinutes($serviceDuration)-&amp;gt;lte($appointmentStart)
) {
    $availableSlots[] = [
        'start' =&amp;gt; $current-&amp;gt;format('H:i'),
        'end'   =&amp;gt; $current-&amp;gt;copy()
            -&amp;gt;addMinutes($serviceDuration)
            -&amp;gt;format('H:i'),
    ];

    $current-&amp;gt;addMinutes($increment);
}

// Move current pointer beyond the booked appointment
if ($current-&amp;gt;lt($appointmentEnd)) {
    $current = $appointmentEnd-&amp;gt;copy();
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;}&lt;/p&gt;

&lt;p&gt;// Handle the remaining time after the last appointment&lt;/p&gt;

&lt;p&gt;while (&lt;br&gt;
    $current-&amp;gt;copy()-&amp;gt;addMinutes($serviceDuration)-&amp;gt;lte($doctorEnd)&lt;br&gt;
) {&lt;br&gt;
    $availableSlots[] = [&lt;br&gt;
        'start' =&amp;gt; $current-&amp;gt;format('H:i'),&lt;br&gt;
        'end'   =&amp;gt; $current-&amp;gt;copy()&lt;br&gt;
            -&amp;gt;addMinutes($serviceDuration)&lt;br&gt;
            -&amp;gt;format('H:i'),&lt;br&gt;
    ];&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$current-&amp;gt;addMinutes($increment);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;}&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;p&gt;AppointmentController&lt;br&gt;
        ↓&lt;br&gt;
AppointmentAvailabilityService&lt;br&gt;
        ↓&lt;br&gt;
Availability calculation&lt;br&gt;
        ↓&lt;br&gt;
Appointment Repository / Model&lt;/p&gt;

&lt;p&gt;This keeps the controller thin and makes the scheduling algorithm easier to test.&lt;/p&gt;

&lt;p&gt;Why a Dedicated Service Class Matters&lt;/p&gt;

&lt;p&gt;Scheduling logic tends to grow quickly.&lt;/p&gt;

&lt;p&gt;Initially, you may only have:&lt;/p&gt;

&lt;p&gt;Doctor availability&lt;br&gt;
+&lt;br&gt;
Booked appointments&lt;/p&gt;

&lt;p&gt;Later, you may need:&lt;/p&gt;

&lt;p&gt;Doctor breaks&lt;br&gt;
+&lt;br&gt;
Week offs&lt;br&gt;
+&lt;br&gt;
Holiday&lt;br&gt;
+&lt;br&gt;
Service duration&lt;br&gt;
+&lt;br&gt;
Buffer time&lt;br&gt;
+&lt;br&gt;
Room availability&lt;br&gt;
+&lt;br&gt;
Multiple doctors&lt;br&gt;
+&lt;br&gt;
Multiple locations&lt;br&gt;
+&lt;br&gt;
Appointment status&lt;br&gt;
+&lt;br&gt;
Cancellation&lt;/p&gt;

&lt;p&gt;If all of this logic lives inside a controller, it becomes difficult to maintain.&lt;/p&gt;

&lt;p&gt;A dedicated service makes the architecture much easier to evolve.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;p&gt;class AppointmentAvailabilityService&lt;br&gt;
{&lt;br&gt;
    public function getAvailableSlots(&lt;br&gt;
        int $doctorId,&lt;br&gt;
        Carbon $date,&lt;br&gt;
        int $serviceDuration&lt;br&gt;
    ): array {&lt;br&gt;
        // availability calculation&lt;br&gt;
    }&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;Now the controller only needs to ask:&lt;/p&gt;

&lt;p&gt;$slots = $availabilityService-&amp;gt;getAvailableSlots(&lt;br&gt;
    $doctorId,&lt;br&gt;
    $date,&lt;br&gt;
    $serviceDuration&lt;br&gt;
);&lt;/p&gt;

&lt;p&gt;Don't Forget Appointment Status&lt;/p&gt;

&lt;p&gt;Another important consideration is appointment status.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;p&gt;confirmed&lt;br&gt;
pending&lt;br&gt;
cancelled&lt;br&gt;
completed&lt;br&gt;
no_show&lt;/p&gt;

&lt;p&gt;A cancelled appointment normally shouldn't block a slot.&lt;/p&gt;

&lt;p&gt;So your query should explicitly define which statuses consume availability.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;p&gt;-&amp;gt;whereIn('status', [&lt;br&gt;
    'confirmed',&lt;br&gt;
    'pending',&lt;br&gt;
])&lt;/p&gt;

&lt;p&gt;Don't simply retrieve every appointment and assume every record blocks the schedule.&lt;/p&gt;

&lt;p&gt;Database Indexing&lt;/p&gt;

&lt;p&gt;Scheduling systems can generate a large number of availability queries.&lt;/p&gt;

&lt;p&gt;If you frequently search appointments by:&lt;/p&gt;

&lt;p&gt;doctor_id&lt;br&gt;
appointment_date&lt;br&gt;
start_time&lt;/p&gt;

&lt;p&gt;then an appropriate composite index can make a significant difference.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;p&gt;$table-&amp;gt;index([&lt;br&gt;
    'doctor_id',&lt;br&gt;
    'appointment_date',&lt;br&gt;
    'start_time',&lt;br&gt;
]);&lt;/p&gt;

&lt;p&gt;The exact indexes should depend on your actual query patterns and database workload.&lt;/p&gt;

&lt;p&gt;Don't blindly add indexes everywhere.&lt;/p&gt;

&lt;p&gt;Think About Race Conditions&lt;/p&gt;

&lt;p&gt;There is another problem that is easy to miss.&lt;/p&gt;

&lt;p&gt;Suppose two patients request the same slot at almost exactly the same time.&lt;/p&gt;

&lt;p&gt;Both requests could see:&lt;/p&gt;

&lt;p&gt;11:50 → 12:35&lt;/p&gt;

&lt;p&gt;as available.&lt;/p&gt;

&lt;p&gt;Both then try to book it.&lt;/p&gt;

&lt;p&gt;This is no longer just a slot-calculation problem.&lt;/p&gt;

&lt;p&gt;It becomes a concurrency problem.&lt;/p&gt;

&lt;p&gt;The final booking operation should therefore re-check availability inside a transaction or use an appropriate locking/constraint strategy.&lt;/p&gt;

&lt;p&gt;Calculating availability and actually reserving the slot should be treated as two different operations.&lt;/p&gt;

&lt;p&gt;The Architecture I Prefer&lt;/p&gt;

&lt;p&gt;For a larger Laravel scheduling application, I'd structure it roughly like this:&lt;/p&gt;

&lt;p&gt;Controller&lt;br&gt;
    │&lt;br&gt;
    ▼&lt;br&gt;
Availability Service&lt;br&gt;
    │&lt;br&gt;
    ├── Doctor Schedule&lt;br&gt;
    ├── Service Duration&lt;br&gt;
    ├── Existing Appointments&lt;br&gt;
    ├── Breaks / Holidays&lt;br&gt;
    └── Business Rules&lt;br&gt;
    │&lt;br&gt;
    ▼&lt;br&gt;
Available Slots&lt;/p&gt;

&lt;p&gt;Then the booking flow becomes:&lt;/p&gt;

&lt;p&gt;Request Slot&lt;br&gt;
     ↓&lt;br&gt;
Calculate Availability&lt;br&gt;
     ↓&lt;br&gt;
User Selects Slot&lt;br&gt;
     ↓&lt;br&gt;
Re-check Availability&lt;br&gt;
     ↓&lt;br&gt;
Transaction&lt;br&gt;
     ↓&lt;br&gt;
Create Appointment&lt;/p&gt;

&lt;p&gt;This separation makes the system easier to reason about and significantly reduces the chance of inconsistent bookings.&lt;/p&gt;

&lt;p&gt;Final Thoughts&lt;/p&gt;

&lt;p&gt;Appointment scheduling is not really a “generate some time slots” problem.&lt;/p&gt;

&lt;p&gt;It is an interval management and constraint-solving problem.&lt;/p&gt;

&lt;p&gt;Once you start thinking in terms of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Continuous availability windows&lt;/li&gt;
&lt;li&gt;Variable service durations&lt;/li&gt;
&lt;li&gt;Interval overlap&lt;/li&gt;
&lt;li&gt;Multiple existing appointments&lt;/li&gt;
&lt;li&gt;Business constraints&lt;/li&gt;
&lt;li&gt;Concurrency&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;the problem becomes much easier to design correctly.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;For me, the biggest lesson is:&lt;/p&gt;

&lt;p&gt;«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.»&lt;/p&gt;

&lt;p&gt;I write more about Laravel, backend architecture, system design, and real-world application development on my portfolio: &lt;a href="https://ajkumar.in" rel="noopener noreferrer"&gt;https://ajkumar.in&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

</description>
      <category>laravel</category>
      <category>php</category>
      <category>appointment</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
