Asynchronous Apex Triggers and Change Data Capture

With any synchronous execution in salesforce we have multiple governor limits e.g. CPU time limit or Heap Size or SOQL limit or DML limit etc that our business logic need to respect. Sometimes the business process become such complex that we use Asynchronous transactions such as Future methods, Queuable Apex and Batch Apex etc to bypass the limits with synchronous processing.

In addition to above, Asynchronous transaction mechanism, Salesforce has introduced Asynchronous Apex Triggers in  Summer ’19.

Async Apex Trigger is nothing but the change event trigger which runs asynchronously after a Database transaction is done. Similar to regular apex trigger but it has only After insert event on change event object instead of SObject.  

For standard SObject, the change event object is SObject Type + ChangeEvent. For Example LeadChangeEvent.

For Custom SObject, it is Custom Object Name+ “__ChangeEvent”.

Async Apex trigger works along with Change Data Capture. In order to use async trigger, first change data capture to be enabled for that particular SObject.

Setup -> Integrations -> Change Data capture

Change Data Capture is one of the offerings from Salesforce Streaming API capabilities like Platform Events. Unlike Platform events, it has transactional boundaries.  CDC notification gets generated when CREATE, UPDATE, DELETE or UNDELETE operation performed on a SObject record.

Also we don’t have to create the event schema model like platform events, it comes out of the box.

Use Cases:

The resource intensive and non-transactional processes could be executed asynchronously in order to reduce transaction time and consumption of resources.

I have considered a simple use case here. When a Lead gets qualified, a task need to be generated and gets assigned to a Sales or Call Center agent to do follow-up on those leads for conversion.

Task creation is not necessarily to be synchronous, it could be after the transaction is completed.

Let’s get into the Code.

Here I have used the trigger framework by http://chrisaldridge.com/triggers/lightweight-apex-trigger-framework/

LeadChangeAsyncTrigger trigger on LeadChangeEvent :

trigger LeadChangeAsyncTrigger on LeadChangeEvent (after insert) {
  // Call the trigger dispatcher and pass it an instance of the LeadAsyncTriggerHandler and Trigger.opperationType
  TriggerDispatcher.Run(new LeadAsyncTriggerHandler(), Trigger.operationType);
}

Depending on the trigger operation type the dispatcher will pass trigger context variable to the trigger handler

TriggerDispatcher.cls :


public class TriggerDispatcher {
  /*
    Call this method from your trigger, passing in an instance of a trigger handler which implements ITriggerHandler.
    This method will fire the appropriate methods on the handler depending on the trigger context.
  */
  public static void Run(ITriggerHandler handler, System.TriggerOperation triggerEvent)
  {
    // Detect the current trigger context and fire the relevant methods on the trigger handler:
  
    switch on triggerEvent {
      when BEFORE_INSERT {
        handler.BeforeInsert(trigger.new);
      } when BEFORE_UPDATE {
        handler.BeforeUpdate(trigger.newMap, trigger.oldMap);
      } when BEFORE_DELETE {
        handler.BeforeDelete(trigger.oldMap);
      } when AFTER_INSERT {
        handler.AfterInsert(Trigger.new);
      } when AFTER_UPDATE {
        handler.AfterUpdate(trigger.newMap, trigger.oldMap);
      } when AFTER_DELETE {
        handler.AfterDelete(trigger.oldMap);
      } when AFTER_UNDELETE {
        handler.AfterUndelete(trigger.oldMap);
      }
    }  
  }
  public class TriggerException extends Exception {}
}

ITriggerHandler interface as part of the trigger framework :

public interface ITriggerHandler {
	
  void BeforeInsert(SObject[] newItems);

  void BeforeUpdate(Map<Id, SObject> newItems, Map<Id, SObject> oldItems);

  void BeforeDelete(Map<Id, SObject> oldItems);

  void AfterInsert(SObject[] newItems);

  void AfterUpdate(Map<Id, SObject> newItems, Map<Id, SObject> oldItems);

  void AfterDelete(Map<Id, SObject> oldItems);

  void AfterUndelete(Map<Id, SObject> oldItems);
    
}

LeadAsyncTriggerHandler .cls :

public class LeadAsyncTriggerHandler implements ITriggerHandler {
  public void BeforeInsert(List<SObject> newItems) {}
  public void BeforeUpdate(Map<Id, SObject> newItems, Map<Id, SObject> oldItems) {
  }
  public void BeforeDelete(Map<Id, SObject> oldItems) {}
  public void AfterInsert(List<SObject> newItems) {
    LeadAsyncService.createTasksForQualifiedLeads(newItems); 
  }
  public void AfterUpdate(Map<Id, SObject> newItems, Map<Id, SObject> oldItems) {}
  public void AfterDelete(Map<Id, SObject> oldItems) {}
  public void AfterUndelete(Map<Id, SObject> oldItems) {}  
}

LeadAsyncService .cls implements the asynchronous processing logic :


Public with sharing class LeadAsyncService {

  public static void createTasksForQualifiedLeads(List <LeadChangeEvent> leadChangeEvents) {
    List <Task> tasksTobeInserted = checkLeadStatus(leadChangeEvents);
    createTasks(tasksTobeInserted);
  }

  public static List<Task> checkLeadStatus(List <LeadChangeEvent> leadChangeEvents) {
    List<Task> tasks = new List<Task>();
    for(LeadChangeEvent leadEvent : leadChangeEvents) {  
      system.debug('leadEvent is:'+ leadEvent);     
      EventBus.ChangeEventHeader eventHeader = leadEvent.ChangeEventHeader;
      if(eventHeader.changetype == 'UPDATE' || eventHeader.changetype == 'CREATE') {
        if(leadEvent.status == 'Qualified') {
          Task task = new Task();
          task.ownerId = eventHeader.CommitUser;
          task.subject = 'Lead needs to be followed up';
          task.whoId = eventHeader.recordIds[0];
          task.ActivityDate = System.Today() + 3;
          tasks.add(task);
        }
      }          
    }
    return tasks;
  }
  
  public static void createTasks(List<Task> tasksTobeInserted) {
    system.debug('tasksTobeInserted is:'+ tasksTobeInserted);
    if(!tasksTobeInserted.isEmpty()) {
      insert tasksTobeInserted;
    }
  }
}

Test class for asynchronous processing logic :

@isTest
Public class TestLeadChangeTrigger {
  @isTest
  static void testCreateLead() {
    //Enable all Change Data Capture entities for notifications only for Test. 
    Test.enableChangeDataCapture();
    Lead testLead = new Lead(LastName='Test Lead', Company='Test Comp', Status = 'Qualified');
    insert testLead;
    //To fire the trigger and deliver the test change event.
    Test.getEventBus().deliver();
    Task[] tasks = [Select id from Task];
    System.assertEquals(1, tasks.size(), 'The change event trigger did not create the expected task.') ;   
  }
  @isTest
  static void testCreateAndUpdateLead() {
    //Enable all Change Data Capture entities for notifications only for Test. 
    Test.enableChangeDataCapture();
    Lead testLead = new Lead(LastName='Test Lead', Company='Test Comp', Status = 'Working - Contacted');
    insert testLead;
    //To fire the trigger and deliver the test change event.
    Test.getEventBus().deliver();
    Lead[] testLeads = [Select Id, Status from Lead];
    testLeads[0].Status = 'Qualified';
    Update testLeads;
    Test.getEventBus().deliver();
    Task[] tasks = [Select id from Task];
    System.assertEquals(1, tasks.size(), 'The change event trigger did not create the expected task.');
  }


}

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.