Skip to content

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

public static void handleAfterInsert(List<UserChangeEvent> uces)

Purpose: Processes User Change Data Capture events when User records are updated with email changes.

Parameters: - uces (List): List of User change events from platform event subscription

Returns: - void

Usage Example:

// Called from UserChangeEventTrigger.trigger
trigger UserChangeEventTrigger on UserChangeEvent (after insert) {
    UserChangeEventTriggerHandler.handleAfterInsert(Trigger.new);
}

Business Logic:

  1. 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);
            }
        }
    }
    
  2. Each CDC event can contain multiple User Ids if changed within same second
  3. Only processes events where Email field changed (uce.Email != null)
  4. Prepares User records with Username = new Email
  5. Collects User Ids for subsequent processing

  6. 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);
        }
    }
    

  7. Key Filter: Only processes portal users (those with AccountId)
  8. Non-portal users are removed from update map
  9. Collects AccountIds for async update

  10. Update Users with 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());
            }
        }
    }
    

  11. ✅ Uses partial success DML (false parameter)
  12. ✅ Checks SaveResults and logs errors
  13. Updates User.Username to match Email
  14. DIFFERENCE from UserTriggerHandler: This actually performs the User update!

  15. Queue Async Account Updates (lines 42-45):

    if (!accountsToUpdate.isEmpty()) {
        UserTriggerHandler.updateAccountEmailsAsync(accountsToUpdate, false);
    }
    

  16. Reuses UserTriggerHandler's @future method
  17. Passes byPassUpdate=false to ensure Account update happens
  18. CRITICAL: This correctly passes false, unlike UserTriggerHandler (which passes true)

✅ 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.trigger subscribes 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

  1. CDC Must Be Enabled for User Object
  2. Change Data Capture must be enabled in Setup for User object
  3. Without CDC enabled, this handler never fires
  4. Verify: Setup → Integrations → Change Data Capture → User object selected
  5. Impact: Handler completely non-functional if CDC not enabled

  6. Dual Handler Conflict Risk (if both handlers active)

  7. UserTriggerHandler (traditional trigger) might also be active
  8. Could cause double-processing or conflicts
  9. Decision Required: Choose ONE approach (CDC or trigger)
  10. Impact: Potential data inconsistency, double DML

HIGH - Address Soon After Go-Live

  1. Error Logging Not Persistent (lines 36-38)
  2. Errors only in debug logs, not Flow_Error_Log__c
  3. Add persistent error logging for monitoring
  4. Impact: Failed updates difficult to track/audit

  5. Tight Coupling to UserTriggerHandler (line 44)

  6. Depends on UserTriggerHandler.updateAccountEmailsAsync()
  7. If UserTriggerHandler changes, this breaks
  8. Consider extracting shared logic to separate utility class
  9. Impact: Maintenance burden, fragile dependency

MEDIUM - Future Enhancement

  1. No Email Validation
  2. Doesn't validate email format before updating
  3. Add regex validation for email format
  4. Impact: Could result in invalid email addresses

  5. CDC Event Timing Uncertainty

  6. CDC events delivered asynchronously (seconds to minutes delay)
  7. Users might experience delay before Account email updates
  8. Document expected sync timing for support team

LOW - Monitor

  1. No Retry Logic
  2. If Account update fails (@future), no automatic retry
  3. Consider adding retry mechanism or manual reconciliation process
  4. Impact: Accounts may stay out of sync until manual correction

  5. Debug Logging Performance (lines 36-38)

  6. Debug statements in loops can impact performance at scale
  7. 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.