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 ...
}