Keeping your Salesforce data clean means enforcing business rules that go way beyond simple unique fields. One of the most common real-world challenges developers face is preventing users from creating duplicate records with conflicting timeframes. Whether you are building a system for equipment reservations, employee PTO requests, or hotel bookings, you cannot afford to have two active records occupying the same exact dates.
Key Points Summary
- Standard Validation Rules cannot query other records; you must use Apex (or complex Flow loops) to validate against existing database rows.
- Date overlap math is incredibly simple once you know the formula:
StartA <= EndB AND EndA >= StartB. - Modern Salesforce development demands that you keep logic out of the
.triggerfile and move it into an Apex Handler class. - Your code must account for updates. If a user is just changing a picklist value on an existing booking, the record shouldn't trigger an overlap error against itself!
The Golden Formula for Date Overlaps
Trying to map out every possible way two dates can overlap (e.g., starting before and ending during, starting during and ending after) will leave you with a massive, unreadable chunk of code. Fortunately, mathematics gives us a shortcut.
Two date ranges, Range A (StartA to EndA) and Range B (StartB to EndB), overlap if and only if:
StartA <= EndB AND EndA >= StartB
By translating this exact logic into your Apex code, you cover every single overlapping scenario perfectly with just one line of boolean math.
Modern Apex Implementation
Writing logic directly inside a Trigger is considered bad practice today. Instead, we use a Trigger Handler. Below is a fully bulkified, modern solution for a custom object called Booking__c.
First, we create a clean, logicless trigger that routes to our handler class on
before insert and before update events.
trigger BookingTrigger on Booking__c (before insert, before update) {
if (Trigger.isBefore) {
BookingTriggerHandler.preventDoubleBooking(Trigger.new, Trigger.isUpdate);
}
}
Next, we build the actual Handler class to process the heavy lifting, query the database safely, and attach the error.
public class BookingTriggerHandler {
public static void preventDoubleBooking(List<Booking__c> newBookings, Boolean isUpdate) {
Set<Id> resourceIds = new Set<Id>();
// Step 1: Collect parent Resource IDs to keep our SOQL query highly targeted
for (Booking__c b : newBookings) {
if (b.Resource__c != null && b.Start_Date__c != null && b.End_Date__c != null) {
resourceIds.add(b.Resource__c);
}
}
if (resourceIds.isEmpty()) return;
// Step 2: Query all existing bookings for those specific resources
List<Booking__c> existingBookings = [
SELECT Id, Resource__c, Start_Date__c, End_Date__c
FROM Booking__c
WHERE Resource__c IN :resourceIds
];
// Step 3: Compare incoming records against existing ones in memory
for (Booking__c newB : newBookings) {
for (Booking__c existingB : existingBookings) {
// CRITICAL: Skip comparing the record to itself during an update
if (isUpdate && newB.Id == existingB.Id) {
continue;
}
// If they belong to the same resource, check for a date collision
if (newB.Resource__c == existingB.Resource__c) {
Boolean isOverlapping = (newB.Start_Date__c <= existingB.End_Date__c) &&
(newB.End_Date__c >= existingB.Start_Date__c);
if (isOverlapping) {
newB.addError('Conflict! A booking already exists from ' +
existingB.Start_Date__c.format() + ' to ' + existingB.End_Date__c.format());
break; // Stop checking further once a duplicate is found
}
}
}
}
}
}
- SOQL Queries Inside Loops: Never write a
SELECTstatement inside aforloop. Our code avoids this by collecting all IDs first, running one query, and comparing the lists in memory. - Forgetting the Self-Check on Updates: If you forget to bypass
newB.Id == existingB.Id, updating a record will cause it to crash because it thinks it is overlapping with its own original database row. - Ignoring Null Dates: Always verify that start and end dates actually contain data before doing comparisons, otherwise you will hit a
NullPointerException.
Frequently Asked Questions
Can I achieve this using Salesforce Record-Triggered Flows instead of Apex?
Yes, but with caveats. You can use a Before-Save Flow that runs a "Get Records" element to look for overlapping dates. However, Flows handle bulk data loads (like Data Loader) less efficiently than Apex. For complex, high-volume objects, Apex remains the most scalable and robust choice.
What if I only want to check overlap for Business Days?
The standard overlap math assumes calendar days. If you only care about business days, you will need to utilize a custom Apex utility method to calculate working days (often utilizing the standard Salesforce BusinessHours class) before comparing the overlap.
How do I handle DateTime fields instead of plain Date fields?
The core logic (StartA <= EndB AND EndA >= StartB) works perfectly for DateTime fields as well. Just ensure that the fields you are comparing share the exact same data type.
- Required Events:
before insert,before update - Method to Block Save:
SObject.addError() - Core Logic Formula:
NewStart <= ExistingEnd AND NewEnd >= ExistingStart - Architecture Standard: Use a Trigger Handler class to keep your code testable, modular, and easy to maintain.