In this Winter ’24 Release recap, I’ll share some of my favorite Apex updates that will definitely be useful for any Salesforce Developer. So, let’s dive right in and explore what exciting things await us.
Unique Signature for Your Queueable Job
By using job signatures, you can effectively minimize resource contention and avoid race conditions when working with async Queueable jobs. The key here is to ensure that only a single instance of your job is enqueued based on its signature.
If you attempt to add multiple Queueable jobs to the processing queue with the same signature, an exception will be thrown for the subsequent jobs during the enqueue operation. This mechanism prevents duplicate jobs from being enqueued and helps maintain the integrity of your job processing flow.
How it works: Use the QueueableDuplicateSignature class methods addId(), addInteger(), and addString() to build a unique signature for your queueable job and store it in the DuplicateSignature property of the AsyncOptions class. Enqueue your job with System.enqueueJob() using the AsyncOptions parameter.
Here is an example:
AsyncOptions options = new AsyncOptions();
options.DuplicateSignature = QueueableDuplicateSignature.Builder()
.addId(UserInfo.getUserId())
.addString('MyQueueable')
.build();
try {
System.enqueueJob(new MyQueueable(), options);
} catch (DuplicateMessageException ex) {
//Exception is thrown if there is already an enqueued job with the same signature
System.debug('exception: ' + ex.getMessage());
}
Configurable Maximum Depth of Chained Queueable Jobs
Starting from the Winter ’24 release, the anticipated feature will become available to all users.
This feature allows you to customize the number of Queueable jobs that can be stacked, surpassing the default limit of five in Developer and Trial Edition orgs. By configuring this number, you gain an enhanced safety mechanism to prevent runaway recursive jobs from consuming the daily async Apex limit.
How it works: you can enqueue jobs using the new System.enqueueJob() overload. The method overload has an optional AsyncOptions parameter to specify the maximum stack depth and the minimum queue delay.
Use these methods in the new System.AsyncInfo class to determine the current and maximum stack depths and to get the minimum queueable delay:
- getCurrentQueueableStackDepth()
- getMaximumQueueableStackDepth()
- getMinimumQueueableDelayInMinutes()
- hasMaxStackDepth()
Let’s move to examples. The first example shows the case where we are hitting the default stack depth limit (it is 5 for Developer and Trial orgs).
public class QueueableWithoutDepthControl implements Queueable {
private Integer depth;
public static void runWithoutAsyncOptions(Integer userDepth) {
System.enqueueJob(new QueueableWithoutDepthControl(userDepth));
}
private QueueableWithoutDepthControl(Integer userDepth) {
depth = userDepth;
}
public void execute(QueueableContext context) {
Integer currentDepth = AsyncInfo.getCurrentQueueableStackDepth();
System.debug('currentDepth: ' + currentDepth + '; depth: ' + depth);
System.enqueueJob(new QueueableWithoutDepthControl(depth));
}
}
The second example uses stack depth to terminate a chained job and, as a result, prevent it from reaching the daily maximum number of asynchronous Apex method executions.
Comparator Interface for Sorting
With Winter ’24 updates, the List class now supports the new Comparator interface, so you can implement different sort orders in your code by using List.sort() with a Comparator parameter.
Here is an example. We have two classes that implement a comparator interface and are used for sorting by name and billing country.
public class AccountComparator {
// Class to compare Accounts by BillingCountry
public class BillingCountryCompare implements Comparator<Account> {
public Integer compare(Account account1, Account account2) {
if (account1?.BillingCountry == null && account2?.BillingCountry == null) {
return 0;
} else if (account1?.BillingCountry == null) {
return -1;
} else if (account2?.BillingCountry == null) {
return 1;
}
return account1.BillingCountry.compareTo(account2.BillingCountry);
}
}
// Class to compare Accounts by CreatedDate
public class CreatedDateCompare implements Comparator<Account> {
public Integer compare(Account account1, Account account2) {
// Guard against null operands for '<' or '>' operators because
// they will always return false and produce inconsistent sorting
Integer result;
if (account1?.CreatedDate == null && account2?.CreatedDate == null) {
result = 0;
} else if (account1?.CreatedDate == null) {
result = -1;
} else if (account2?.CreatedDate == null) {
result = 1;
} else if (account1.CreatedDate < account2.CreatedDate) {
result = -1;
} else if (account1.CreatedDate > account2.CreatedDate) {
result = 1;
} else {
result = 0;
}
return result;
}
}
}
To use these classes for sorting, we should pass our class instances as a parameter to a sort method. An example is below:
@IsTest
static void testCreatedDateCompare() {
List<Account> accountsList = [SELECT Id, Name, BillingCountry, NumberOfEmployees, CreatedDate FROM Account LIMIT 10];
accountsList.sort(new AccountComparator.CreatedDateCompare());
Assert.areEqual('UNITED OIL INSTALLATIONS', accountsList[0].Name);
Assert.areEqual('EDGE INSTALLATION', accountsList[1].Name);
Assert.areEqual('GRAND HOTELS SLA', accountsList[2].Name);
}
@IsTest
static void testCountryCompare() {
List<Account> accountsList = [SELECT Id, Name, BillingCountry, NumberOfEmployees, CreatedDate FROM Account LIMIT 10];
accountsList.sort(new AccountComparator.BillingCountryCompare());
Assert.areEqual('Austria', accountsList[0].BillingCountry);
Assert.areEqual('France', accountsList[1].BillingCountry);
Assert.areEqual('Germany', accountsList[2].BillingCountry);
}
Conclusion
Here are my top Apex updates from the Salesforce Winter ’24 release. In the next article, we will share hot LWC updates from Winter ’24. Stay in touch!