Change Data Capture (CDC) Code Examples

Complete, working code examples for Change Data Capture patterns. All examples are copy-paste ready and follow Salesforce best practices.

Overview

Change Data Capture (CDC) provides real-time change notifications for Salesforce records. CDC events are published automatically when records are created, updated, deleted, or undeleted.

Related Patterns:

Examples

Example 1: Basic CDC Trigger Handler

Pattern: Trigger-based CDC processing Use Case: Processing CDC events directly in Apex triggers Complexity: Basic Related Patterns: Trigger-Based CDC Processing

Problem: You need to process Contact change events to sync data to an external system. Trigger processes events and calls external API for each change.

Solution:

/**
 * Trigger handler for Contact CDC events
 */
trigger ContactChangeEventTrigger on ContactChangeEvent (after insert) {
    ContactChangeEventHandler.handleEvents(Trigger.new);
}

/**
 * Handler class for Contact CDC events
 */
public class ContactChangeEventHandler {
    
    public static void handleEvents(List<ContactChangeEvent> events) {
        List<ContactChangeEvent> eventsToProcess = new List<ContactChangeEvent>();
        
        for (ContactChangeEvent event : events) {
            // Filter events by change type
            if (event.ChangeEventHeader.getChangeType() == 'CREATE' ||
                event.ChangeEventHeader.getChangeType() == 'UPDATE') {
                eventsToProcess.add(event);
            }
        }
        
        if (!eventsToProcess.isEmpty()) {
            // Process events asynchronously
            ContactSyncQueueable job = new ContactSyncQueueable(eventsToProcess);
            System.enqueueJob(job);
        }
    }
}

/**
 * Queueable class to sync Contact changes to external system
 */
public class ContactSyncQueueable implements Queueable {
    
    private List<ContactChangeEvent> events;
    
    public ContactSyncQueueable(List<ContactChangeEvent> events) {
        this.events = events;
    }
    
    public void execute(QueueableContext context) {
        for (ContactChangeEvent event : events) {
            try {
                // Extract change data
                String changeType = event.ChangeEventHeader.getChangeType();
                Id recordId = event.ChangeEventHeader.getRecordIds()[0];
                
                // Query current record state
                Contact contact = [
                    SELECT Id, Name, Email, Phone
                    FROM Contact
                    WHERE Id = :recordId
                    WITH SECURITY_ENFORCED
                ];
                
                // Sync to external system
                syncToExternalSystem(contact, changeType);
                
            } catch (Exception e) {
                // Log error
                System.debug('ERROR: Failed to sync Contact ' + event.Id + ': ' + e.getMessage());
                // Store failed event for replay
                storeFailedEvent(event, e);
            }
        }
    }
    
    private void syncToExternalSystem(Contact contact, String changeType) {
        // Prepare API callout
        HttpRequest req = new HttpRequest();
        req.setEndpoint('callout:Named_Credential/api/contacts/sync');
        req.setMethod('POST');
        req.setHeader('Content-Type', 'application/json');
        
        // Build request body
        Map<String, Object> requestBody = new Map<String, Object>{
            'salesforceId' => contact.Id,
            'name' => contact.Name,
            'email' => contact.Email,
            'phone' => contact.Phone,
            'changeType' => changeType
        };
        req.setBody(JSON.serialize(requestBody));
        
        // Perform callout
        Http http = new Http();
        HttpResponse res = http.send(req);
        
        if (res.getStatusCode() != 200) {
            throw new SyncException('Sync failed: ' + res.getStatus());
        }
    }
    
    private void storeFailedEvent(ContactChangeEvent event, Exception e) {
        // Store failed event in custom object for replay
        CDC_Event_Log__c log = new CDC_Event_Log__c(
            Event_ID__c = event.Id,
            Object_Type__c = 'Contact',
            Change_Type__c = event.ChangeEventHeader.getChangeType(),
            Record_ID__c = event.ChangeEventHeader.getRecordIds()[0],
            Error_Message__c = e.getMessage(),
            Status__c = 'Failed'
        );
        insert log;
    }
    
    public class SyncException extends Exception {}
}

Explanation:

Usage: CDC events are automatically published when Contact records change. No manual triggering needed.


Example 2: CDC with Platform Events

Pattern: Combining CDC with Platform Events Use Case: Complex event-driven workflows with CDC change detection Complexity: Intermediate Related Patterns: Platform Event Integration with CDC

Problem: Contact changes trigger CDC events, which publish Platform Events with enriched data. Multiple subscribers process Platform Events for different purposes.

Solution:

/**
 * Trigger handler for Contact CDC events
 * Publishes Platform Events for complex processing
 */
trigger ContactChangeEventTrigger on ContactChangeEvent (after insert) {
    ContactChangeEventHandler.handleEvents(Trigger.new);
}

/**
 * Handler class that publishes Platform Events from CDC events
 */
public class ContactChangeEventHandler {
    
    public static void handleEvents(List<ContactChangeEvent> events) {
        List<Contact_Change_Event__e> platformEvents = new List<Contact_Change_Event__e>();
        
        for (ContactChangeEvent cdcEvent : events) {
            // Extract CDC event data
            String changeType = cdcEvent.ChangeEventHeader.getChangeType();
            Id recordId = cdcEvent.ChangeEventHeader.getRecordIds()[0];
            
            // Query related data for enrichment
            Contact contact = [
                SELECT Id, Name, Email, AccountId, Account.Name
                FROM Contact
                WHERE Id = :recordId
                WITH SECURITY_ENFORCED
            ];
            
            // Create Platform Event with enriched data
            Contact_Change_Event__e platformEvent = new Contact_Change_Event__e();
            platformEvent.Contact_ID__c = contact.Id;
            platformEvent.Contact_Name__c = contact.Name;
            platformEvent.Contact_Email__c = contact.Email;
            platformEvent.Account_ID__c = contact.AccountId;
            platformEvent.Account_Name__c = contact.Account?.Name;
            platformEvent.Change_Type__c = changeType;
            platformEvent.Changed_Fields__c = JSON.serialize(cdcEvent.ChangeEventHeader.getChangedFields());
            
            platformEvents.add(platformEvent);
        }
        
        // Publish Platform Events
        if (!platformEvents.isEmpty()) {
            List<Database.SaveResult> results = EventBus.publish(platformEvents);
            
            // Handle publish errors
            for (Integer i = 0; i < results.size(); i++) {
                if (!results[i].isSuccess()) {
                    System.debug('ERROR: Failed to publish Platform Event: ' + results[i].getErrors());
                }
            }
        }
    }
}

Explanation:


Example 3: CDC Error Handling and Retry

Pattern: CDC event error handling with retry logic Use Case: Handling CDC event processing failures Complexity: Advanced Related Patterns: CDC Error Handling and Replay

Problem: CDC event processing fails due to external API timeout. Failed events are logged, and replay mechanism processes them after API recovers.

Solution:

/**
 * Handler with error handling and retry
 */
public class ContactChangeEventHandler {
    
    public static void handleEvents(List<ContactChangeEvent> events) {
        List<CDC_Event_Log__c> eventLogs = new List<CDC_Event_Log__c>();
        
        for (ContactChangeEvent event : events) {
            try {
                // Process event
                processEvent(event);
                
                // Log success
                eventLogs.add(createEventLog(event, 'Success', null));
                
            } catch (Exception e) {
                // Log failure
                eventLogs.add(createEventLog(event, 'Failed', e.getMessage()));
            }
        }
        
        // Insert event logs
        if (!eventLogs.isEmpty()) {
            insert eventLogs;
        }
    }
    
    private static void processEvent(ContactChangeEvent event) {
        String changeType = event.ChangeEventHeader.getChangeType();
        Id recordId = event.ChangeEventHeader.getRecordIds()[0];
        
        // Query record
        Contact contact = [
            SELECT Id, Name, Email
            FROM Contact
            WHERE Id = :recordId
            WITH SECURITY_ENFORCED
        ];
        
        // Sync to external system with retry
        syncWithRetry(contact, changeType, 0);
    }
    
    private static void syncWithRetry(Contact contact, String changeType, Integer retryCount) {
        try {
            // Perform sync
            HttpRequest req = new HttpRequest();
            req.setEndpoint('callout:Named_Credential/api/sync');
            req.setMethod('POST');
            req.setBody(JSON.serialize(new Map<String, Object>{
                'id' => contact.Id,
                'name' => contact.Name,
                'email' => contact.Email,
                'changeType' => changeType
            }));
            
            Http http = new Http();
            HttpResponse res = http.send(req);
            
            if (res.getStatusCode() != 200) {
                throw new SyncException('Sync failed: ' + res.getStatus());
            }
            
        } catch (Exception e) {
            // Retry on transient errors
            if (retryCount < 3 && isTransientError(e)) {
                // Wait with exponential backoff
                Integer delayMs = (Integer) Math.pow(2, retryCount) * 1000; // 1s, 2s, 4s
                
                // Enqueue retry job
                ContactSyncRetryQueueable retryJob = new ContactSyncRetryQueueable(
                    contact.Id,
                    changeType,
                    retryCount + 1
                );
                System.enqueueJob(retryJob);
            } else {
                // Max retries reached or non-transient error
                throw e;
            }
        }
    }
    
    private static Boolean isTransientError(Exception e) {
        // Check if error is transient (timeout, connection error, etc.)
        String errorMsg = e.getMessage();
        return errorMsg.contains('timeout') || 
               errorMsg.contains('connection') || 
               errorMsg.contains('503') ||
               errorMsg.contains('502');
    }
    
    private static CDC_Event_Log__c createEventLog(
        ContactChangeEvent event, 
        String status, 
        String errorMessage
    ) {
        return new CDC_Event_Log__c(
            Event_ID__c = event.Id,
            Object_Type__c = 'Contact',
            Change_Type__c = event.ChangeEventHeader.getChangeType(),
            Record_ID__c = event.ChangeEventHeader.getRecordIds()[0],
            Status__c = status,
            Error_Message__c = errorMessage,
            Event_Data__c = JSON.serialize(event)
        );
    }
    
    public class SyncException extends Exception {}
}

/**
 * Queueable for retrying failed syncs
 */
public class ContactSyncRetryQueueable implements Queueable {
    
    private Id contactId;
    private String changeType;
    private Integer retryCount;
    
    public ContactSyncRetryQueueable(Id contactId, String changeType, Integer retryCount) {
        this.contactId = contactId;
        this.changeType = changeType;
        this.retryCount = retryCount;
    }
    
    public void execute(QueueableContext context) {
        Contact contact = [
            SELECT Id, Name, Email
            FROM Contact
            WHERE Id = :contactId
            WITH SECURITY_ENFORCED
        ];
        
        ContactChangeEventHandler.syncWithRetry(contact, changeType, retryCount);
    }
}

Explanation: