Building a Shortlet Booking System in Laravel with Filament Admin

HomeBlogBuilding a Shortlet Booking System in Laravel with Filament Admin

Web Development 2026-08-01

Building a Shortlet Booking System in Laravel with Filament Admin

Laravel Filament Booking System Database Design Race Conditions Payment Integration PHP Property Management Backend Development Nigeria Tech

When people hear "booking system," they usually picture something simple. A calendar, a few available dates, a form to reserve one. That mental model works fine until you actually try to build one for a real business handling real money and real guests, and then you realize a booking system is really a small distributed problem hiding inside what looks like a basic CRUD feature. This post covers how I built the shortlet booking module as part of the adefeyproperties.com Laravel rebuild, why I split availability and bookings into two separate concerns instead of one, how payment status ties into the whole flow, and how the Filament admin side turned into the actual operational hub for the business.

Why Shortlets Needed Their Own System

Adefey Homes had two very different kinds of listings living on the same platform. Long-term rentals and outright sales, which follow a fairly predictable lifecycle, a property gets listed, someone inquires, negotiations happen offline or through the platform, and eventually the listing gets marked as taken. Shortlets are a completely different animal. A single property can be booked by dozens of different guests across a year, each for a few days or weeks at a time, and the platform needs to know exactly which dates are open at any given moment, handle payment before confirming a reservation, and prevent two people from booking the same dates by accident.

WordPress genuinely could not do this well. There are shortlet and vacation rental plugins out there, but they tend to be built for a single type of business, standalone vacation rental sites, not bolted onto a broader real estate platform that also handles sales and long-term rentals. Trying to force one of those plugins into coexisting with everything else on adefeyproperties.com would have meant fighting the plugin's assumptions constantly. Building it natively in Laravel meant I could design the data model around exactly what the business needed, nothing more, nothing forced in from a generic plugin's opinion of how bookings should work.

The Core Design Decision: Separating Availability from Bookings

This is probably the single most important architectural decision in the whole system, and it's one I got wrong on my first attempt before correcting it, so I want to walk through both versions honestly.

My first instinct, which I think is most people's first instinct, was to just query the bookings table directly whenever I needed to check if a property was available for a given date range. Something like, find any bookings for this property where the requested check-in and check-out dates overlap with an existing booking's dates, and if none exist, the property is available. This works, technically, for the simple case of "is this property booked or not."

The problem showed up almost immediately once real requirements came in. Property owners needed to block off dates for maintenance, cleaning turnarounds between guests, or personal use of the property, without those blocks looking like actual paying bookings in the reporting and revenue dashboards. If I only had a bookings table, I'd either have to create fake "booking" records with no payment attached just to block dates, which would pollute revenue reports and booking counts with non-revenue entries, or I'd need a separate flag on the bookings table to distinguish real bookings from blocks, which starts making a single table carry two very different meanings depending on a flag, always a sign that you actually need two different concepts modeled separately.

So I split it. A bookings table that only ever represents an actual guest reservation, tied to a guest, a payment record, and a status. And a separate availability_blocks table that represents any period a property is not available, whether that's because of an actual booking, or because of an owner-initiated block for maintenance, cleaning, or personal use. When a booking is confirmed, the system automatically creates a corresponding entry in the availability blocks table, but the two concepts remain distinct at the data level.

This separation paid off almost immediately in ways I hadn't fully anticipated when I made the change. Revenue reports could now query the bookings table cleanly without needing to filter out fake entries. Availability checks became simpler too, since checking whether a property is free for a date range just means checking the availability blocks table, one query, one clear concept, rather than needing to reason about bookings with different status flags meaning different things.

Handling the Overlap Problem Correctly

Even with the cleaner data model, checking date overlaps correctly is one of those things that looks trivial and has a surprising number of edge cases if you don't think it through carefully. The core logic for detecting whether two date ranges overlap comes down to a fairly standard comparison, a new booking's check-in date must be before any existing block's check-out date, and the new booking's check-out date must be after any existing block's check-in date, for there to be a genuine overlap.

The edge case that catches people out is the boundary condition. Should a guest be allowed to check in on the exact same day another guest checks out? For a shortlet property, the answer is usually yes, since the cleaning and turnaround happens within that same day, and disallowing same-day check-in after checkout would waste a night of potential revenue for every single turnover. But this means the overlap check has to use strict inequality rather than inclusive comparison on the boundary dates, otherwise the system will incorrectly flag a valid same-day turnover as a conflict and block a legitimate booking.

I wrote a dedicated test suite specifically around these boundary conditions, back-to-back bookings on the exact same turnover day, a booking that exactly matches an existing block's dates, a booking that partially overlaps at the start or end of an existing block, because these are exactly the kind of bugs that don't show up in casual manual testing but absolutely will show up in production the first time two guests both try to book around the same turnover date.

Locking Down Race Conditions

Here's a problem that a lot of booking system tutorials completely gloss over: what happens when two people try to book the exact same dates at the exact same moment? This isn't a theoretical concern, it's a very real scenario for any property that gets meaningful traffic, especially around popular dates.

If your availability check and your booking creation are two separate steps without any locking, you get a classic race condition. Both requests check availability at nearly the same instant, both see the dates as free because neither booking has been committed to the database yet, and both proceed to create a booking for the same dates. Now you have a double-booked property and an angry guest showing up to a place that's already occupied.

I handled this using database-level locking rather than trying to solve it purely in application logic. When a booking request comes in, I wrap the availability check and the booking creation inside a database transaction, and I use a row-level lock on the relevant availability records during that transaction. This means that if two requests do arrive at nearly the same time, the second one will be forced to wait for the first transaction to complete before its own availability check runs, at which point it will correctly see the dates as no longer available and reject the booking, rather than both requests racing through and both succeeding.

This is one of those pieces of infrastructure work that's invisible when it's working correctly and catastrophic when it's missing, and I'd genuinely encourage anyone building a booking system of any kind to treat this as a non-negotiable part of the design rather than something to add later if it becomes a problem. By the time it becomes a visible problem, you've already got an angry guest and a property owner who's lost trust in the platform.

Payment Status and the Booking Lifecycle

A booking isn't really confirmed until payment is confirmed, and I modeled the booking lifecycle around explicit states rather than a simple boolean "is this booking confirmed" flag. A booking moves through pending, meaning a guest has selected dates and initiated payment but it hasn't been confirmed yet, paid, meaning payment has been verified and the booking is locked in, and then either completed, once the guest's stay has passed, or cancelled, if the guest or the property manager cancels before the stay.

Only bookings in the paid state actually create a hard availability block that prevents other guests from booking the same dates. A pending booking holds a short, time-limited soft reservation, typically fifteen minutes, giving the guest time to complete payment without permanently locking out other potential guests indefinitely on a booking that might never actually complete. If payment isn't confirmed within that window, a scheduled job releases the soft hold automatically, freeing those dates back up.

This soft-hold-then-release pattern solved a real problem I saw in an earlier, simpler version of the system, where guests who started a booking but abandoned the payment step partway through would leave the dates looking unavailable to everyone else indefinitely, effectively locking out real paying guests because of an abandoned checkout. The time-limited hold with automatic release fixed that without requiring any manual intervention from staff.

Payment verification followed the same principle I've applied on other projects, never trust the frontend's claim that payment succeeded. Every payment is independently verified server-side against the payment gateway before a booking transitions from pending to paid, with a webhook listener as a backup path in case the frontend verification call doesn't complete for any reason, whether that's a closed browser tab or a network interruption right after payment.

The Filament Admin Side: Where the Real Operations Happen

Building the booking logic correctly on the backend is only half the story. The other half is giving the people actually running the business a usable interface to manage what's happening day to day, and this is where the Filament admin panel became genuinely central to how the business operates, not just a technical nicety.

I built a dedicated bookings resource in Filament that shows upcoming check-ins and check-outs prominently, since knowing who's arriving and departing on a given day is the single most operationally relevant piece of information for anyone managing shortlet properties. Staff can filter bookings by property, by date range, and by payment status, which matters a lot when following up on any pending bookings that are sitting close to their soft-hold expiration.

I also built a calendar view widget directly into the Filament dashboard, showing a visual month-by-month breakdown of bookings and blocks across all properties at once. This turned out to be one of the most-used parts of the whole admin panel, because it lets staff see at a glance which properties have open availability for a given period without clicking into each property individually, which matters a lot when someone calls in asking about availability for a specific week and staff need an answer in seconds, not minutes.

For payment issues specifically, I built a dedicated view flagging any booking that's been sitting in pending status unusually long, or any booking where payment verification failed after the guest reported completing payment on their end, since payment gateway hiccups do happen occasionally and staff need a way to manually investigate and resolve these edge cases rather than the guest simply being told to try again with no visibility into what went wrong.

Availability blocks got their own simple interface too, letting property owners or staff quickly mark a property as unavailable for maintenance or cleaning without going through the full booking flow, directly creating an availability block record with a reason field so there's a clear audit trail of why a property was taken off the market for a given period.

What I Learned Building This

The biggest lesson from this piece of the rebuild is that booking systems are fundamentally about correctly modeling time and state, not about building a pretty calendar interface. The calendar UI is the easy, visible part. The hard part, and the part that actually determines whether the system is trustworthy, is getting the underlying data model and concurrency handling right so that the numbers on that calendar are always accurate no matter how many people are interacting with the system at once.

I also came away with a much stronger appreciation for building admin tooling as a first-class part of the product rather than an afterthought. It would have been easy to treat the Filament admin panel as just a way to view database records, but investing real thought into what staff actually need to see and act on quickly, upcoming check-ins, availability at a glance, flagged payment issues, turned it into something that genuinely changes how efficiently the business runs day to day, not just a technical management interface bolted on for developer convenience.

If you're building something similar, my honest advice is to spend real time upfront thinking through the edge cases before writing the booking logic itself. Same-day turnovers, abandoned checkouts, simultaneous booking attempts, owner-initiated blocks that shouldn't count as revenue. None of these are exotic scenarios, they're all things that will happen in normal operation of any real booking system, and it's much cheaper to design for them from the start than to retrofit the fixes after a guest has already shown up to a double-booked property.

← Back to Blog Have a project in mind? Let's talk →