Salesforce Apex Queueable Interface Implementation

The Salesforce Apex Queueable Interface enables the asynchronous execution of Apex jobs, offering similarities to @Future annotated Apex classes. However, the distinctive feature of a queueable interface is its ability to be monitored through the Salesforce user interface or by querying the AsyncApexJob table.

The utility of a queueable interface becomes apparent when initiating an asynchronous process from another asynchronous process. Unlike @Future annotated classes, which are restricted from execution within a batch job, a queueable interface seamlessly integrates with batch jobs.

You might be curious about the practicality of a queueable interface for everyday administrators or developers. In my perspective, its value shines in scenarios where additional processing needs to be handled without impeding users actively navigating the Salesforce user interface.

For instance, consider the need to perform rollup calculations on a record when related records undergo modifications. This could involve updating Community Contact records based on changes to a User record within a Community or assigning a territory value to Contacts owned by a User when the territory value on the User record changes. Instead of executing this logic directly in the trigger or trigger handler, initiating a queueable interface allows processing based on resource availability without affecting the user experience.

Here are some personal examples of using the queueable interface:

  • Making an API request to an external endpoint.
  • Reassigning related records when ownership changes on a topmost record.
  • Calculating the last interaction date on Contacts after Tasks/Events are linked to a Contact.
  • Calculating project hours after time allocation records are linked to a given project.

Previously, I demonstrated how to reassign Contacts and Opportunities to an Account owner if the OwnerId was changed outside the user interface. In that example, all logic was executed within a trigger. However, transferring the primary business logic to a queueable interface can be achieved through the creation of the following Apex Class:

/*
	Created by: Greg Hacic
	Last Update: 7 February 2019 by Greg Hacic
	Questions?: greg@interactiveties.com
	
	Notes:
		- queueable interface enables the asynchronous execution of Apex jobs that can be monitored
		- reassigns Contacts and Opportunities linked to an Account
*/
public class queueReassign implements queueable {
	
	private final Map<Id, Id> previousOwnerIds = new Map<Id, Id>(); //map of Account.Id -> old Account.OwnerId
	
	//constructor
	public queueReassign(Map<Id, Id> passedMap) {
		previousOwnerIds = passedMap; //assign the passed map
	}
	
	//executes the queueable logic
	public void execute(QueueableContext qc) {
		
		List<Contact> contactUpdates = new List<Contact>; //list of Contact objects to be updated
		List<Opportunity> opportunityUpdates = new List<Opportunity>; //list of Opportunity objects to be updated
		
		for (Account a : [SELECT Id, OwnerId, (SELECT Id, OwnerId FROM Contacts), (SELECT Id, OwnerId FROM Opportunities WHERE IsClosed = False) FROM Account WHERE Id in :previousOwnerIds.keySet()]) { //query for Contacts and Opportunities for specific Accounts
			Id oldOwnerId = previousOwnerIds.get(a.Id); //grab the old OwnerId value for the Account from our map
			for (Contact c : a.Contacts) { //for all related Contacts
				if (c.OwnerId == oldOwnerId) { //if the Contact is assigned to the old Account Owner
					contactUpdates.add(new Contact(Id = c.Id, OwnerId = a.OwnerId)); //construct a new Contact and add it to our list for updating
				}
			}
			for (Opportunity o : a.Opportunities) { //for all related Opportunities
				if (o.OwnerId == oldOwnerId) { //if the Opportunity is assigned to the old Account Owner
					opportunityUpdates.add(new Opportunity(Id = o.Id, OwnerId = a.OwnerId)); //construct a new Opportunity and add it to our list for updating
				}
			}
		}
		
		//update the Contacts
		if (!contactUpdates.isEmpty()) { //if the contactUpdates is not empty
			List<Database.SaveResult> updateContactResults = Database.update(contactUpdates, false); //update the records and allow for some failures
		}
		
		//update the Opportunities
		if (!opportunityUpdates.isEmpty()) { //if the opportunityUpdates is not empty
			List<Database.SaveResult> updateOpportunityResults = Database.update(opportunityUpdates, false); //update the records and allow for some failures
		}
	
	}

}

Of course, using a queueable interface still requires that the interface be started in some way. A trigger is perfectly suitable and that code is below:

/*
	Created by: Greg Hacic
	Last Update: 7 February 2019 by Greg Hacic
	Questions?: greg@interactiveties.com
	
	Notes:
		- Account object trigger - Trigger.isAfter && Trigger.isUpdate contexts 
*/
trigger accountTrigger on Account (after update) {
	
	Map<Id, Id> oldOwnerIds = new Map<Id, Id>(); //map of Account.Id -> pretrigger Account.OwnerId
	
	for (Account a : Trigger.new) { //for all records
		if (a.OwnerId != Trigger.oldMap.get(a.Id).OwnerId) { //if the OwnerId is different
			oldOwnerIds.put(a.Id, Trigger.oldMap.get(a.Id).OwnerId); //populate the map using the Account Id as the key and the Old OwnerId as the value
		}
	}
	
	if (!oldOwnerIds.isEmpty()) { //if the map is not empty
		System.enqueueJob(new queueReassign(oldOwnerIds)); //queue up the process to reassign related Contacts and Opportunities
	}

}
  • Notice that the trigger will still be triggered when Account records are updated. However, by executing the Contact and Opportunity updates from a queueable interface rather than within the trigger itself, the execution time for the trigger logic is naturally faster.

Another aspect to observe in the queueable interface is the use of the constructor method. In the provided example, a constructor is employed to retrieve the Account Ids passed from the trigger. This choice was made to ensure that only records related to the Accounts with ownership changes are processed. It’s essential to note that the use of a constructor is not mandatory within a queueable interface. An alternative approach could involve incorporating a query directly into the execute method of the queueable interface to identify records requiring reassignment. Perhaps a topic to explore and share at another time.