Class Name: SafeUpdateService¶
Last Updated: 2025-10-22 Source Code: https://github.com/AANP-IT/I2C.Salesforce.Metadata/blob/STAGING/force-app/main/default/classes/SafeUpdateService.cls
API Name: SafeUpdateService Type: Service (Invocable) Test Coverage: To be determined
Business Purpose¶
The SafeUpdateService class provides a Flow-invocable method for performing partial-success DML updates with automatic error logging. This service enables Flows to:
- Update records without failing the entire transaction on individual record errors
- Automatically log failures to Flow_Error_Log__c for monitoring
- Continue processing valid records when some records have validation errors
- Return success/failure status to Flow for decision logic
This supports resilient Flow automation that handles data quality issues gracefully.
Class Overview¶
- Scope/Sharing:
with sharing- Respects record-level security - Key Responsibilities:
- Perform partial-success DML updates
- Log failed records to Flow_Error_Log__c
- Return error status to Flow
- Support multiple object types
Inner Classes¶
RequestWrapper¶
public class RequestWrapper {
@InvocableVariable(required=true)
public List<SObject> recordsToUpdate;
@InvocableVariable(required=false)
public String flowName;
}
Fields:
- recordsToUpdate (List
Public Methods¶
safeUpdateRecords¶
@InvocableMethod(label='Safe Update Records')
public static List<Boolean> safeUpdateRecords(List<RequestWrapper> requests)
Purpose: Updates records with partial success, logging errors without failing transaction.
Parameters:
- requests (List
Returns: List<Boolean> - One boolean per request indicating if ANY errors occurred
Business Logic:
- Partial Success Update (line 10):
falseparameter enables partial success (allOrNone=false)-
Individual record failures don't stop others
-
Error Detection (lines 12-32):
- Iterates through SaveResult array
-
For each failed record:
- Extracts record Id and object type
- Populates object-specific lookup fields (lines 20-24)
- Creates Flow_Error_Log__c record
- Sets hasErrors flag
-
Object-Specific Lookups (lines 20-24):
if (objectName == 'Subscription__c') { errorLogRecord.SubscriptionId__c = recordId; } else if (objectName == 'Membership__c') { errorLogRecord.MembershipId__c = recordId; } else if (objectName == 'Account') { errorLogRecord.AccountId__c = recordId; } else if (objectName == 'Order') { errorLogRecord.Order__c = recordId; } else if (objectName == 'OrderSummary') { errorLogRecord.Order_Summary__c = recordId; } - Populates lookup fields for common object types
-
Objects not in list only get generic ObjectName__c
-
Error Log Insertion (lines 35-37):
- Inserts all error logs in single DML operation
-
No error handling if log insertion fails
-
Return Value Construction (lines 39-43):
- BUG: Returns same hasErrors value for ALL requests
- Should track errors per request, not globally
Issues/Concerns: - 🚨 CRITICAL BUG (lines 40-42): Returns same boolean for all requests - If ANY request has errors, ALL return true - Cannot distinguish which specific request failed - Should track hasErrors per request - ⚠️ No Error Log Insert Handling: Line 36 insert can fail silently - ⚠️ Global Error Flag: Single hasErrors for all requests (line 6) - ⚠️ Limited Object Support: Only 5 object types have lookup fields - ✅ Partial Success: Properly uses allOrNone=false - ✅ Bulk Error Logging: Single insert for all error logs
Correct Implementation:
@InvocableMethod(label='Safe Update Records')
public static List<Boolean> safeUpdateRecords(List<RequestWrapper> requests) {
List<Flow_Error_Log__c> errorLogs = new List<Flow_Error_Log__c>();
List<Boolean> resultsToReturn = new List<Boolean>();
for (RequestWrapper req : requests) {
Boolean hasErrors = false;
Database.SaveResult[] results = Database.update(req.recordsToUpdate, false);
for (Integer i = 0; i < results.size(); i++) {
if (!results[i].isSuccess()) {
hasErrors = true;
for (Database.Error err : results[i].getErrors()) {
String recordId = (String)req.recordsToUpdate[i].get('Id');
String objectName = req.recordsToUpdate[i].getSObjectType().getDescribe().getName();
Flow_Error_Log__c errorLogRecord = new Flow_Error_Log__c();
if (objectName == 'Subscription__c') { errorLogRecord.SubscriptionId__c = recordId; }
else if (objectName == 'Membership__c') { errorLogRecord.MembershipId__c = recordId; }
else if (objectName == 'Account') { errorLogRecord.AccountId__c = recordId; }
else if (objectName == 'Order') { errorLogRecord.Order__c = recordId; }
else if (objectName == 'OrderSummary') { errorLogRecord.Order_Summary__c = recordId; }
errorLogRecord.ObjectName__c = objectName;
errorLogRecord.ErrorMessage__c = err.getMessage();
errorLogRecord.FlowRunDateTime__c = System.now();
errorLogRecord.FlowName__c = req.flowName;
errorLogs.add(errorLogRecord);
}
}
}
resultsToReturn.add(hasErrors);
}
if (!errorLogs.isEmpty()) {
try {
insert errorLogs;
} catch (DmlException e) {
System.debug(LoggingLevel.ERROR, 'Failed to insert error logs: ' + e.getMessage());
}
}
return resultsToReturn;
}
Flow Usage Example:
Flow: Update Membership Records
Get Records: memberships (Membership__c collection)
Loop: For each membership
Assignment: Update membership fields
Action: Safe Update Records
recordsToUpdate: {!memberships}
flowName: "Update Membership Records"
Store Output: hasErrors
Decision: Check if errors occurred
If hasErrors = true
Send Email Alert to Admin
Else
Display Success Message
Dependencies¶
Salesforce Objects¶
- Flow_Error_Log__c (Custom Object)
- Fields: ObjectName__c, ErrorMessage__c, FlowRunDateTime__c, FlowName__c, SubscriptionId__c, MembershipId__c, AccountId__c, Order__c, Order_Summary__c
- Access: Create (INSERT)
Custom Settings/Metadata¶
- None
Other Classes¶
- Similar to:
DailyMembershipUtil.safeUpdate()
Design Patterns¶
- Invocable Method Pattern: Flow integration
- Partial Success DML: allOrNone=false for resilient updates
- Error Aggregation: Collects errors before bulk insert
- Object-Type Switching: Handles multiple object types dynamically
Governor Limits Considerations¶
Current Impact (Per Transaction)¶
- DML Statements: 2 per request (1 update + 1 error log insert)
- DML Rows: Variable based on records and failures
- SOQL Queries: 0
- Heap Size: Proportional to number of errors
Scalability Analysis¶
- ✅ Bulk Error Logging: Single insert for all errors
- ⚠️ Per-Request Processing: Processes each request separately (not bulkified across requests)
- ⚠️ No Maximum Validation: Doesn't validate total records across requests
Error Handling¶
Exception Types Thrown¶
- None - Method doesn't throw exceptions
Exception Types Caught¶
- None - No try-catch blocks
Error Handling Gaps¶
- Silent Error Log Failure: Line 36 insert can fail without notification
- No DML Exception Handling: Update failures not caught
- No Input Validation: Doesn't check for null/empty inputs
Security Considerations¶
Sharing Model¶
- WITH SHARING: Respects record-level security
- Updates only records user has access to
FLS Considerations¶
- ⚠️ No FLS Validation: Doesn't check field-level security
- Relies on Database.update to enforce security
Test Class Requirements¶
@IsTest
public class SafeUpdateServiceTest {
@TestSetup
static void setup() {
Account acc = new Account(Name = 'Test Account');
insert acc;
List<Membership__c> memberships = new List<Membership__c>();
for (Integer i = 0; i < 5; i++) {
memberships.add(new Membership__c(
Account_Name__c = acc.Id,
Status__c = 'Active',
Start_Date__c = Date.today()
));
}
insert memberships;
}
@IsTest
static void testSafeUpdateRecords_Success() {
List<Membership__c> memberships = [SELECT Id, Status__c FROM Membership__c];
for (Membership__c m : memberships) {
m.Status__c = 'Expired';
}
SafeUpdateService.RequestWrapper req = new SafeUpdateService.RequestWrapper();
req.recordsToUpdate = memberships;
req.flowName = 'Test Flow';
Test.startTest();
List<Boolean> results = SafeUpdateService.safeUpdateRecords(new List<SafeUpdateService.RequestWrapper>{req});
Test.stopTest();
Assert.areEqual(1, results.size(), 'Should return one result');
Assert.isFalse(results[0], 'Should have no errors');
List<Flow_Error_Log__c> logs = [SELECT Id FROM Flow_Error_Log__c];
Assert.isTrue(logs.isEmpty(), 'Should have no error logs');
}
@IsTest
static void testSafeUpdateRecords_PartialFailure() {
List<Membership__c> memberships = [SELECT Id FROM Membership__c];
// Create one invalid update (missing required field)
Membership__c invalidMembership = new Membership__c(Id = memberships[0].Id);
// Assume Status__c is required, set to null
SafeUpdateService.RequestWrapper req = new SafeUpdateService.RequestWrapper();
req.recordsToUpdate = new List<Membership__c>{invalidMembership};
req.flowName = 'Test Partial Failure';
Test.startTest();
List<Boolean> results = SafeUpdateService.safeUpdateRecords(new List<SafeUpdateService.RequestWrapper>{req});
Test.stopTest();
// Due to bug, this tests current behavior
Assert.isTrue(results[0], 'Should have errors');
List<Flow_Error_Log__c> logs = [SELECT Id, ErrorMessage__c FROM Flow_Error_Log__c];
Assert.isFalse(logs.isEmpty(), 'Should have error logs');
}
}
Changes & History¶
| Date | Author | Description |
|---|---|---|
| Unknown | Original Developer | Initial implementation |
| (Current) | - | Documentation added |
Pre-Go-Live Concerns¶
🚨 CRITICAL¶
- Return Value Bug (lines 40-42): Returns same boolean for all requests
- Fix to track errors per request separately
- Breaking change for existing Flows
HIGH¶
- Silent Error Log Failure: No try-catch on error log insert (line 36)
- Add error handling to prevent silent failures
- Log to System.debug if insert fails
MEDIUM¶
- Limited Object Support: Only 5 object types have lookup fields
- Add more object types or make configurable
- No Input Validation: Doesn't validate null/empty inputs
- Add validation for recordsToUpdate
LOW¶
- Global Error Flag: Single hasErrors for all requests (design issue fixed in recommended code)
Maintenance Notes¶
📋 Monitoring Recommendations¶
- Query Flow_Error_Log__c daily for failures
- Track which Flows have highest error rates
- Monitor object types with most failures
🔧 Future Enhancement Opportunities¶
- Configurable Object Mappings: Use custom metadata for object-to-lookup mappings
- Return Detailed Results: Return list of failed record IDs per request
- Retry Logic: Add automatic retry for transient errors
- Bulk Optimization: Bulkify across multiple requests
⚠️ Breaking Change Risks¶
- Fixing return value bug changes Flow behavior
- Adding exceptions changes error handling model
🔗 Related Components¶
- DailyMembershipUtil.safeUpdate(): Similar pattern
- Flow_Error_Log__c: Error logging target
- Flow Automations: Multiple flows use this service
Business Owner¶
Primary Contact: Salesforce Development Team Technical Owner: Salesforce Development Team Last Reviewed: [Date]
Documentation Status: ✅ Complete Code Review Status: 🚨 CRITICAL - Return value bug affects all callers Test Coverage: Test class needed