If you're hitting limits in your synchronous code, Queueable Apex is usually the first tool I recommend. I've spent years fixing performance issues in messy orgs, and this is the middle ground developers keep forgetting about. It's lighter to write than Batch Apex and far more capable than a simple @future method.
A trigger starts failing on the 101 SOQL limit, or a callout takes too long, and the work has to come off the transaction. Deciding between Asynchronous Apex in Salesforce types can be tricky, but Queueable is where I land most of the time.
Why I prefer Queueable Apex over future methods
I used to use @future for everything back in the day. It has real flaws though. You can't pass complex objects like lists of sObjects or custom classes, so you're stuck with primitives. Queueable Apex fixes that: you pass actual objects into the constructor, which saves a lot of unpacking when the logic gets complicated.
Monitoring is the other reason I reach for it. Fire off a Queueable job and you get a Job ID back immediately, so you can find it in the Apex Jobs queue and see whether it succeeded or failed. With @future you're mostly hoping for the best. You can also chain jobs, which matters when managing Salesforce large data volumes and things have to happen in a specific order.
A simple implementation that actually works
Here's how I usually structure these classes. The example updates some Account descriptions in the background, and the pattern is what matters. Note how the data comes in through the constructor.
public class AccountUpdateQueueable implements Queueable {
private List<Account> accountsToUpdate;
private String updateReason;
public AccountUpdateQueueable(List<Account> accounts, String reason) {
this.accountsToUpdate = accounts;
this.updateReason = reason;
}
public void execute(QueueableContext context) {
try {
for (Account acc : accountsToUpdate) {
acc.Description = 'Updated: ' + updateReason + ' on ' + DateTime.now();
}
if (!accountsToUpdate.isEmpty()) {
update accountsToUpdate;
}
} catch (Exception e) {
// Log this to a custom object so you don't lose the error
System.debug('Error in Queueable: ' + e.getMessage());
}
}
}
Kicking it off is a one-liner:
Id jobId = System.enqueueJob(new AccountUpdateQueueable(myAccounts, 'Bulk Update'));
Best practices for Queueable Apex in the wild
I've seen teams get into trouble by chaining jobs infinitely. Salesforce caps this for a reason. A production org lets you chain a long sequence of jobs, while a Developer Edition or trial org allows far fewer. If you're chaining, put a "max depth" check in your code or you'll eventually hit a wall.
Use the Transaction Finalizer interface if you need to handle errors or clean up after a job. It's more reliable than trying to wrap everything in a try-catch block inside the execute method.
A few things I keep in mind when I'm building with Queueable Apex:
- Use
Limits.getDmlStatements()andLimits.getQueries()so you know how close to the edge you are. - Bulkify. Don't pass one record if you can pass fifty; it's much cheaper for the platform.
- If you're updating a User (setup object) and an Account (non-setup object), do it in separate jobs to avoid that annoying "MIXED_DML_OPERATION" error.
- Use
Test.startTest()andTest.stopTest()in your tests. The job won't actually run until you hit that stop call.
The async limit trips people up too. I've written before about how to stay under asynchronous limits, and it's worth a read if you're planning on running thousands of these jobs a day. You don't want to be the one who shuts down the org's automation by blowing through the 24-hour limit.
Key takeaways
- Queueable Apex handles complex data types like Lists and Sets, unlike @future methods.
- Every job returns a Job ID, so it's easy to track in the UI.
- Chaining gives you sequential processing, but it needs guardrails against infinite loops.
- An enqueued job only runs during a unit test once you reach
Test.stopTest().
Queueable Apex is mostly about making the app feel faster for the user. Nobody wants to wait five seconds for a page to save while your code talks to an external API or updates 200 related records. Move that logic to the background, and the code holds up better for it.
Leave a Comment