Future Methods & Queueable Apex
Asynchronous Apex runs later, in its own transaction, with higher governor limits (200 SOQL, 60 s CPU, 12 MB heap).
23.1 Why asynchronous Apex?
Asynchronous Apex runs later, in its own transaction, with higher governor limits (200 SOQL, 60 s CPU, 12 MB heap). Use it for callouts from triggers, long-running work, mixed DML separation, and processing large data volumes.
Primitives & collections of primitives Any serializable member state (sObjects, custom types) Query or Iterable Returns a job ID Yes (AsyncApexJob) Yes (CronTrigger) Yes (from finish) Can start batch/queueable Via queueable/batch Good (+ Finalizers) Up to 50 million records
23.2 Future methods
Rules: must be static void; parameters must be primitives, arrays, or collections of primitives (pass IDs, then re-query); no guaranteed execution order; cannot call a future method from another future or batch method; max 50 per transaction. For new code, prefer Queueable.
public class AccountGeocoder {
@future(callout=true)
public static void geocode(Set<Id> accountIds) {
List<Account> accs = [SELECT Id, BillingStreet, BillingCity FROM Account WHERE Id IN :accountIds];
for (Account a : accs) {
// HTTP callout to a geocoding service (§27) ...
a.Description = 'Geocoded on ' + Datetime.now();
}
update accs;
}
}
// called from a trigger (after insert) — triggers cannot make synchronous callouts
AccountGeocoder.geocode(Trigger.newMap.keySet());23.3 Queueable Apex
public class ContactEnrichmentJob implements Queueable, Database.AllowsCallouts {
private List<Contact> contacts;
private Integer attempt;
public ContactEnrichmentJob(List<Contact> contacts, Integer attempt) {
this.contacts = contacts;
this.attempt = attempt;
}
public void execute(QueueableContext ctx) {
System.debug('Job ' + ctx.getJobId() + ' attempt ' + attempt);
// ... callouts and processing ...
update contacts;
// chaining: enqueue the next job (only ONE child job from an executing queueable)
if (attempt < 3 && !Test.isRunningTest()) {
System.enqueueJob(new ContactEnrichmentJob(contacts, attempt + 1));
}
}
}
Id jobId = System.enqueueJob(new ContactEnrichmentJob(myContacts, 1));
AsyncApexJob job = [SELECT Status, NumberOfErrors FROM AsyncApexJob WHERE Id = :jobId];23.4 Queueable options: delay, depth, and duplicate signatures
In Developer Edition and trial orgs, chain depth is limited (5); in other orgs, chains are unlimited unless you set a maximum.
// delay start by 0–10 minutes
System.enqueueJob(new ContactEnrichmentJob(myContacts, 1), 5);
// AsyncOptions: limit chain depth and prevent duplicates
AsyncOptions opts = new AsyncOptions();
opts.MaximumQueueableStackDepth = 10;
opts.DuplicateSignature = QueueableDuplicateSignature.Builder()
.addId(accountId)
.addString('ContactEnrichment')
.build();
System.enqueueJob(new ContactEnrichmentJob(myContacts, 1), opts);
// inside a job
Integer depth = AsyncInfo.getCurrentQueueableStackDepth();
Boolean hasMax = AsyncInfo.hasMaxStackDepth();23.5 Transaction Finalizers
A Finalizer runs after a Queueable finishes — even if it failed with an uncatchable LimitException — enabling logging and retries:
A finalizer runs in its own transaction with its own limits; it can enqueue one Queueable (e.g., a retry). Only one finalizer can be attached per Queueable execution.
public class EnrichmentFinalizer implements Finalizer {
private List<Contact> contacts;
private Integer attempt;
public EnrichmentFinalizer(List<Contact> contacts, Integer attempt) {
this.contacts = contacts; this.attempt = attempt;
}
public void execute(FinalizerContext ctx) {
if (ctx.getResult() == ParentJobResult.UNHANDLED_EXCEPTION) {
Exception e = ctx.getException();
insert new Log__c(Message__c = 'Job ' + ctx.getAsyncApexJobId() + ' failed: ' + e.getMessage());
if (attempt < 3) {
System.enqueueJob(new ContactEnrichmentJob(contacts, attempt + 1)); // retry
}
}
}
}
// attach inside the queueable's execute()
public void execute(QueueableContext ctx) {
System.attachFinalizer(new EnrichmentFinalizer(contacts, attempt));
// ... work that might fail ...
}