Class Name: UserChangeEventTriggerHandler¶
Last Updated: 2025-09-10 Source Code: UserChangeEventTriggerHandler.cls
API Name: UserChangeEventTriggerHandler Type: Service (Change Data Capture Handler) Test Coverage: TBD
Business Purpose¶
Listens to User Change Data Capture (CDC) events and synchronizes email changes between User and Account records for portal users. This is an alternative approach to traditional trigger-based synchronization (see UserTriggerHandler), using platform events for near-real-time processing. When a User's email changes, this handler updates the User.Username to match and queues the related Account.PersonEmail to be updated asynchronously.
Class Overview¶
Scope and Sharing¶
- Sharing Model:
with sharing- Respects record-level security - Access Modifier:
public - Interfaces Implemented: None (static helper class)
- Trigger Type: Change Data Capture (CDC) - subscribes to UserChangeEvent
Key Responsibilities¶
- Process UserChangeEvent platform events (Change Data Capture)
- Detect email changes from CDC event data
- Update User.Username to match new email (portal users only)
- Queue Account.PersonEmail updates via @future method
- Handle bulk CDC events (multiple Users in single event)
- Filter out non-portal users (Users without AccountId)
Public Methods¶
handleAfterInsert¶
Purpose: Processes User Change Data Capture events when User records are updated with email changes.
Parameters:
- uces (List
Returns:
- void
Usage Example:
// Called from UserChangeEventTrigger.trigger
trigger UserChangeEventTrigger on UserChangeEvent (after insert) {
UserChangeEventTriggerHandler.handleAfterInsert(Trigger.new);
}
Business Logic:
- Process Change Events (lines 2-18):
Map<Id, String> accountsToUpdate = new Map<Id, String>(); Map<Id, User> usersToUpdate = new Map<Id, User>(); Set<Id> userIdsUpdated = new Set<Id>(); for(UserChangeEvent uce: uces){ EventBus.ChangeEventHeader ceh = uce.ChangeEventHeader; List<Id> recordIds = ceh.recordids; // Multiple Ids can be in one event if(uce.Email != null){ for(Id recordId: recordIds){ usersToUpdate.put(recordId, new User(Id= recordId, Username = uce.Email)); userIdsUpdated.add(recordId); } } } - Each CDC event can contain multiple User Ids if changed within same second
- Only processes events where Email field changed (
uce.Email != null) - Prepares User records with Username = new Email
-
Collects User Ids for subsequent processing
-
Query Portal Users (lines 20-29):
List<User> users = [SELECT Id, Email, AccountId FROM User WHERE Id IN :userIdsUpdated]; for(User u: users){ if(u.AccountId != null){ accountsToUpdate.put(u.AccountId, u.Email); } else if(usersToUpdate.containsKey(u.Id)){ //The user is not a portal user, remove from the map of updateable users usersToUpdate.remove(u.Id); } } - Key Filter: Only processes portal users (those with AccountId)
- Non-portal users are removed from update map
-
Collects AccountIds for async update
-
Update Users with Error Handling (lines 32-41):
- ✅ Uses partial success DML (
falseparameter) - ✅ Checks SaveResults and logs errors
- Updates User.Username to match Email
-
DIFFERENCE from UserTriggerHandler: This actually performs the User update!
-
Queue Async Account Updates (lines 42-45):
- Reuses UserTriggerHandler's @future method
- Passes
byPassUpdate=falseto ensure Account update happens - CRITICAL: This correctly passes
false, unlike UserTriggerHandler (which passestrue)
✅ Advantages Over UserTriggerHandler:
- Actually updates User.Username (not commented out)
- Correctly passes false to ensure Account updates happen
- Better error handling with partial success DML
- Filters out non-portal users proactively
- Uses CDC for near-real-time async processing
⚠️ Issues/Concerns: - SOQL in CDC Handler (line 21): Query User records - acceptable but consumes query - Debug Logging Only (lines 36-38): Errors only logged to debug, not persisted - Reuses Future Method: Depends on UserTriggerHandler.updateAccountEmailsAsync() - ✅ Bulk Pattern: Handles multiple Users efficiently - ✅ Error Handling: Checks SaveResults and logs failures - ✅ Partial Success: Individual failures don't block other updates
Dependencies¶
Apex Classes¶
- UserTriggerHandler: Reuses
updateAccountEmailsAsync()method - Dependency Note: Tightly coupled to UserTriggerHandler's @future method
Salesforce Objects¶
- User (Standard Object)
- Fields accessed:
Id,Email,Username,AccountId - Operations: Read (query), Update (with error handling)
- Account (Standard Object)
- Fields accessed:
Id,PersonEmail - Operations: Update (via UserTriggerHandler @future method)
Platform Events¶
- UserChangeEvent (Standard Change Data Capture Event)
- Auto-generated when User records change
- Contains changed field values and ChangeEventHeader
- Trigger:
UserChangeEventTrigger.triggersubscribes to these events
External Services¶
- Change Data Capture: Salesforce CDC platform
- Must be enabled for User object
- Publishes events when User records change
- Near-real-time event delivery (typically within seconds)
Design Patterns¶
Change Data Capture (CDC) Pattern¶
- Subscribes to platform events for record changes
- Decoupled from direct trigger execution
- Asynchronous processing by design
- Better scalability for high-volume updates
Event-Driven Architecture¶
- Reacts to UserChangeEvent platform events
- Separate from traditional trigger workflow
- Can coexist with UserTrigger if both are active
Partial Success DML¶
- Uses
Database.update(records, false)for fault tolerance - Checks SaveResults individually
- Continues processing even if some records fail
Bulk Processing¶
- Handles multiple change events in single invocation
- Each event can contain multiple User Ids
- Processes collections efficiently
Governor Limits Considerations¶
SOQL Queries: 1 (User query to check AccountId) DML Operations: 2 (User update + @future Account update) CPU Time: Low - simple Map/List operations Heap Size: Low - small collections
CDC-Specific Limits: - CDC events delivered asynchronously (not real-time) - Event retention: 3 days - Can receive batched events (multiple Users per event) - No control over event timing/delivery
Bulkification: ✅ Yes - processes multiple Users and events together Async Processing: ✅ CDC events processed asynchronously + @future for Account updates
Scalability: - ✅ CDC events auto-batch during high volume - ✅ Handles multiple User Ids per event - ✅ Partial success prevents total failure - ⚠️ Depends on CDC event delivery timing
Error Handling¶
Strategy: Partial success + debug logging Logging: Debug logs only (not persistent) User Notifications: None
Current Error Handling (lines 32-41):
List<Database.SaveResult> srs = Database.update(usersToUpdate.values(), false);
for(Database.SaveResult sr: srs){
if(!sr.isSuccess()){
for(Database.Error err : sr.getErrors()){
System.debug('Error updating User: ' + err.getStatusCode() + ': ' + err.getMessage());
}
}
}
Recommendations:
// Improve error logging with persistent storage
List<Database.SaveResult> srs = Database.update(usersToUpdate.values(), false);
List<Flow_Error_Log__c> errorLogs = new List<Flow_Error_Log__c>();
for(Integer i = 0; i < srs.size(); i++){
if(!srs[i].isSuccess()){
List<User> usersList = new List<User>(usersToUpdate.values());
User failedUser = usersList[i];
for(Database.Error err : srs[i].getErrors()){
System.debug(LoggingLevel.ERROR, 'Error updating User ' + failedUser.Id + ': ' + err.getStatusCode() + ': ' + err.getMessage());
// Persist error for monitoring
errorLogs.add(new Flow_Error_Log__c(
FlowName__c = 'UserChangeEventTriggerHandler',
ErrorMessage__c = err.getMessage(),
ObjectName__c = 'User',
FlowRunDateTime__c = System.now()
));
}
}
}
if(!errorLogs.isEmpty()){
try {
insert errorLogs;
} catch (Exception e) {
System.debug(LoggingLevel.ERROR, 'Failed to log errors: ' + e.getMessage());
}
}
Security Considerations¶
Sharing Rules: ✅ Respects sharing via with sharing
Field-Level Security: ⚠️ No FLS checks - assumes CDC event contains accessible fields
CRUD Permissions: ✅ CDC only fires for fields user can access
Input Validation: ⚠️ No email format validation
CDC Security: - CDC events only published for records user has access to - Platform handles field-level security automatically - Handler runs in system context via trigger
Security Concerns: - No validation of new email format - No duplicate email check - Assumes CDC data is valid
Test Class¶
Test Class: UserChangeEventTriggerHandlerTest.cls (likely exists) Coverage: TBD Test Scenarios to Cover: - CDC event with email change updates User.Username - CDC event with email change updates Account.PersonEmail - Portal user email change syncs to Account - Non-portal user (no AccountId) excluded from updates - Multiple Users in single CDC event - Partial failure: Some Users succeed, others fail - CDC event without email change (uce.Email == null) - Edge case: User with AccountId but Account doesn't exist
Testing CDC Events:
@IsTest
static void testUserEmailChangeViaChangeEvent() {
// Setup
Account acc = TestDataFactory.getAccountRecord(true);
User portalUser = TestDataFactory.getPortalUser(true);
portalUser.AccountId = acc.Id;
update portalUser;
Test.startTest();
// Simulate CDC event (requires Test.enableChangeDataCapture())
// Or test handler directly with mock UserChangeEvent
UserChangeEvent uce = new UserChangeEvent();
uce.Email = 'newemail@example.com';
// Note: UserChangeEvent construction in tests can be complex
UserChangeEventTriggerHandler.handleAfterInsert(new List<UserChangeEvent>{uce});
Test.stopTest();
// Verify User.Username updated
User updatedUser = [SELECT Username FROM User WHERE Id = :portalUser.Id];
Assert.areEqual('newemail@example.com', updatedUser.Username);
// Verify Account.PersonEmail updated (after @future completes)
Account updatedAcc = [SELECT PersonEmail FROM Account WHERE Id = :acc.Id];
Assert.areEqual('newemail@example.com', updatedAcc.PersonEmail);
}
⚠️ Pre-Go-Live Concerns¶
🚨 CRITICAL - Fix Before Go-Live¶
- CDC Must Be Enabled for User Object
- Change Data Capture must be enabled in Setup for User object
- Without CDC enabled, this handler never fires
- Verify: Setup → Integrations → Change Data Capture → User object selected
-
Impact: Handler completely non-functional if CDC not enabled
-
Dual Handler Conflict Risk (if both handlers active)
- UserTriggerHandler (traditional trigger) might also be active
- Could cause double-processing or conflicts
- Decision Required: Choose ONE approach (CDC or trigger)
- Impact: Potential data inconsistency, double DML
HIGH - Address Soon After Go-Live¶
- Error Logging Not Persistent (lines 36-38)
- Errors only in debug logs, not Flow_Error_Log__c
- Add persistent error logging for monitoring
-
Impact: Failed updates difficult to track/audit
-
Tight Coupling to UserTriggerHandler (line 44)
- Depends on UserTriggerHandler.updateAccountEmailsAsync()
- If UserTriggerHandler changes, this breaks
- Consider extracting shared logic to separate utility class
- Impact: Maintenance burden, fragile dependency
MEDIUM - Future Enhancement¶
- No Email Validation
- Doesn't validate email format before updating
- Add regex validation for email format
-
Impact: Could result in invalid email addresses
-
CDC Event Timing Uncertainty
- CDC events delivered asynchronously (seconds to minutes delay)
- Users might experience delay before Account email updates
- Document expected sync timing for support team
LOW - Monitor¶
- No Retry Logic
- If Account update fails (@future), no automatic retry
- Consider adding retry mechanism or manual reconciliation process
-
Impact: Accounts may stay out of sync until manual correction
-
Debug Logging Performance (lines 36-38)
- Debug statements in loops can impact performance at scale
- Consider removing or using LoggingLevel check
Maintenance Notes¶
Complexity: Medium Recommended Review Schedule: Quarterly Key Maintainer Notes:
CDC vs Traditional Trigger:¶
This class provides an alternative to UserTriggerHandler: - UserTriggerHandler: Traditional trigger, synchronous (but has bugs) - UserChangeEventTriggerHandler: CDC-based, asynchronous, working correctly
Decision Required: - Only ONE should be active in production - Current state: Both may be active (potential conflicts) - Recommended: Use UserChangeEventTriggerHandler (this class) - it's functional
Critical Dependencies:¶
- Change Data Capture must be enabled for User object
- UserTriggerHandler.updateAccountEmailsAsync() must remain available
- Consider extracting shared @future method to utility class
Integration Points:¶
- Subscribes to UserChangeEvent platform events
- Called from UserChangeEventTrigger.trigger (after insert on UserChangeEvent)
- Calls UserTriggerHandler.updateAccountEmailsAsync() for Account sync
Testing Considerations:¶
- CDC events challenging to test - may need mock objects
Test.enableChangeDataCapture()required in tests- Must use
Test.startTest()/Test.stopTest()for @future - Consider testing handler logic directly with constructed events
Performance Characteristics:¶
- Async by design: CDC events processed after commit
- Batched automatically: Platform batches events during high volume
- Eventual consistency: Account updates happen after User updates complete
Documentation Status: ✅ Complete Code Review Status: ⚠️ HIGH - Verify CDC enabled, resolve dual-handler conflict Test Coverage: Requires verification Related Classes: - UserTriggerHandler (alternative trigger-based approach) - UserTrigger (traditional User trigger - may conflict)
Recommendation: This class is more reliable than UserTriggerHandler and should be the primary approach if CDC is enabled. Disable UserTriggerHandler.handleAfterUpdate() if this CDC handler is active.