Maintaining high data quality often requires business validations that standard duplicate rules cannot handle. A classic scenario is booking validation: ensuring that an asset, room, or user schedule does not have overlapping start and end dates. By leveraging a bulkified before insert, before update Apex trigger, you can validate date ranges in real time and block conflicting records with custom error messages.
1. Understanding the Date Overlap Formula
Two date ranges (StartA to EndA) and (StartB to EndB) overlap if and only if:
Checking only for exact start-date or end-date equality is insufficient because a new booking could sit entirely inside an existing range or completely encompass it. Your SOQL query and logic must evaluate the full range.
2. Writing the Bulkified Trigger Handler
To avoid hitting SOQL governor limits, we follow the Trigger Handler pattern: collect the boundary dates from incoming records, run a single targeted SOQL query, and compare records in memory.
EventDuplicateChecker.cls)
public with sharing class EventDuplicateChecker {
public static void checkDateOverlap(List<Event__c> newEvents, Map<Id, Event__c> oldEventMap) {
Date minFromDate;
Date maxToDate;
List<Event__c> validEvents = new List<Event__c>();
// 1. Basic validation and collecting date boundaries
for (Event__c evt : newEvents) {
if (evt.From_Date__c == null || evt.To_Date__c == null) {
continue;
}
if (evt.From_Date__c > evt.To_Date__c) {
evt.addError('To Date must be equal to or later than From Date.');
continue;
}
validEvents.add(evt);
if (minFromDate == null || evt.From_Date__c < minFromDate) {
minFromDate = evt.From_Date__c;
}
if (maxToDate == null || evt.To_Date__c > maxToDate) {
maxToDate = evt.To_Date__c;
}
}
if (validEvents.isEmpty() || minFromDate == null || maxToDate == null) {
return;
}
// 2. Query only existing records that could potentially overlap
Set<Id> currentIds = oldEventMap != null ? oldEventMap.keySet() : new Set<Id>();
List<Event__c> existingEvents = [
SELECT Id, From_Date__c, To_Date__c
FROM Event__c
WHERE Id NOT IN :currentIds
AND From_Date__c <= :maxToDate
AND To_Date__c >= :minFromDate
WITH USER_MODE
];
// 3. In-memory comparison against existing records and other records in the same batch
for (Integer i = 0; i < validEvents.size(); i++) {
Event__c currentEvt = validEvents[i];
// Compare against database records
for (Event__c existing : existingEvents) {
if (currentEvt.From_Date__c <= existing.To_Date__c && currentEvt.To_Date__c >= existing.From_Date__c) {
currentEvt.addError('This schedule overlaps with an existing booking from ' +
existing.From_Date__c.format() + ' to ' + existing.To_Date__c.format() + '.');
break;
}
}
// Compare against sibling records within the same Trigger.new batch
for (Integer j = i + 1; j < validEvents.size(); j++) {
Event__c siblingEvt = validEvents[j];
if (currentEvt.From_Date__c <= siblingEvt.To_Date__c && currentEvt.To_Date__c >= siblingEvt.From_Date__c) {
siblingEvt.addError('Conflict detected: Another record in this batch shares an overlapping date range.');
}
}
}
}
}
EventTrigger.trigger)
trigger EventTrigger on Event__c (before insert, before update) {
if (Trigger.isBefore && (Trigger.isInsert || Trigger.isUpdate)) {
EventDuplicateChecker.checkDateOverlap(Trigger.new, Trigger.oldMap);
}
}
- Context: Must run in
before insertandbefore updateto block invalid records prior to database commit. - Query Selectivity: Always bound your SOQL query by
minFromDateandmaxToDateinstead of querying all rows in the object. - Update Self-Exclusion: Exclude
Trigger.oldMap.keySet()in the query so updating a record does not flag itself as a duplicate. - Batch Self-Comparison: Compare records within
Trigger.newagainst each other to prevent duplicate entries inside a single bulk upload.
3. Critical Traps & Best Practices
Writing
SELECT Id, From_Date__c, To_Date__c FROM Event__c without a WHERE clause will quickly hit the 50,000 query row governor limit as your org grows. Always filter the query using the earliest and latest dates present in the active transaction.
- Verify End Date ≥ Start Date: Always enforce that
To_Date__cis greater than or equal toFrom_Date__cbefore performing range comparisons. - Include Parent / Resource Dimensions: If bookings belong to specific rooms or assets, add the lookup filter (e.g.,
WHERE Resource__c IN :resourceIds) to prevent false positives across different resources. - Enforce Security: Use
WITH USER_MODEin queries to respect Object-Level and Field-Level permissions automatically.
Summary
Preventing overlapping dates in Salesforce requires a robust overlap formula, efficient SOQL bounding, and in-memory batch validation. By moving your validation logic into a dedicated Trigger Handler class and excluding active record IDs during updates, you protect data integrity without risking governor limit exceptions.