03 - Async Apex and Platform Events¶
When sync Apex isn't enough: long-running, callouts in trigger context, scheduled, large-volume.
Decision matrix¶
| Need | Tool |
|---|---|
| Quick fire-and-forget callout / DML after current transaction | @future |
| Pass complex types or chain jobs | Queueable |
| Process millions of records | Batch |
| Run on a schedule | Schedulable (often kicks off Batch) |
| React to events from any source (UI, API, Apex, Flow) | Platform Events |
| Big object batch | Batch with custom iterable |
Future Methods¶
public class MyClass {
@future
public static void sendNotification(Set<Id> userIds) {
// ...
}
@future(callout=true)
public static void doHttpCallout(String url) {
Http h = new Http();
HttpRequest req = new HttpRequest();
req.setEndpoint(url);
req.setMethod('GET');
HttpResponse res = h.send(req);
}
}
// Call:
MyClass.sendNotification(new Set<Id>{userId1, userId2});
Constraints¶
- Must be
static - Must return
void - Args: only primitives, collections of primitives, Lists/Maps of primitives. No sObjects (use IDs and re-query in the future method).
- Cannot call from another @future method
- Cannot call from Batch's execute / Visualforce constructor
- 50 future calls per transaction
- 250,000 / day org-wide
When to use¶
- HTTP callout from a trigger (sync triggers can't make callouts)
- Decouple slow downstream from current transaction
- Higher governor limits (some limits double for async)
Queueable Apex¶
The modern @future replacement.
public class MyQueueable implements Queueable {
private List<Id> recordIds;
public MyQueueable(List<Id> ids) {
this.recordIds = ids;
}
public void execute(QueueableContext context) {
// ... can pass sObjects/custom types in constructor
// Can chain by enqueueing another job
if (someCondition) {
System.enqueueJob(new AnotherQueueable());
}
}
}
// Call:
Id jobId = System.enqueueJob(new MyQueueable(ids));
Properties¶
- Pass complex objects (sObjects, custom classes) via constructor / instance vars
- Chain unlimited jobs (each
executecan enqueue more) - 50 jobs per transaction
- 250,000 / day
- Implement
Database.AllowsCalloutsfor HTTP callouts
Queueable + callout¶
public class MyJob implements Queueable, Database.AllowsCallouts {
public void execute(QueueableContext ctx) {
// HTTP callout allowed here
}
}
Batch Apex¶
Process millions of records in chunks.
public class AccountUpdateBatch implements Database.Batchable<sObject> {
public Database.QueryLocator start(Database.BatchableContext bc) {
return Database.getQueryLocator('SELECT Id, Industry FROM Account WHERE Industry = null');
}
public void execute(Database.BatchableContext bc, List<Account> scope) {
for (Account a : scope) {
a.Industry = 'Other';
}
update scope;
}
public void finish(Database.BatchableContext bc) {
// Send completion email
}
}
// Run:
Database.executeBatch(new AccountUpdateBatch(), 200); // batch size 200 (default)
Properties¶
start()returns QueryLocator (50M record max) or Iterableexecute()runs once per batch (default 200 records, max 2,000)finish()runs once at end (good for notifications, follow-up jobs)- 5 concurrent batch jobs per org
- 250,000 batch executions per day org-wide
Stateful batch¶
public class MyBatch implements Database.Batchable<sObject>, Database.Stateful {
Integer counter = 0; // preserved across batches
public Database.QueryLocator start(...) { }
public void execute(...) { counter++; }
public void finish(...) { System.debug(counter); }
}
Database.Stateful preserves member variables across batch executions; otherwise, instance vars are reset per batch.
Batch + callout¶
public class MyBatch implements Database.Batchable<sObject>, Database.AllowsCallouts {
// Callouts allowed in execute()
}
Schedulable Apex¶
public class NightlyBatchScheduler implements Schedulable {
public void execute(SchedulableContext ctx) {
Database.executeBatch(new AccountUpdateBatch());
}
}
// Schedule:
String cron = '0 0 2 * * ?'; // every day at 2:00 AM
System.schedule('Nightly Account Update', cron, new NightlyBatchScheduler());
Cron syntax: seconds minutes hours dayOfMonth month dayOfWeek [year].
Platform Events¶
Pub/sub for event-driven architecture across Apex, Flow, external systems, LWC.
Define an event¶
Like a custom object: My_Event__e (suffix __e). Add custom fields.
Publish from Apex¶
My_Event__e evt = new My_Event__e(Account_Id__c=accId, Reason__c='Stage Changed');
Database.SaveResult sr = EventBus.publish(evt);
Subscribe in Apex (trigger on the event)¶
trigger MyEventTrigger on My_Event__e (after insert) {
for (My_Event__e evt : Trigger.new) {
// React
}
}
Subscribe in LWC¶
import { subscribe, unsubscribe, onError } from 'lightning/empApi';
this.subscription = await subscribe('/event/My_Event__e', -1, callback);
Subscribe in Flow¶
Use a Platform Event-Triggered Flow.
Use cases¶
- Decouple parts of your app
- Cross-org integration via Streaming API
- Real-time UI updates in LWC
- Replace tightly-coupled trigger logic
Choosing async type¶
| Scenario | Tool |
|---|---|
| Trigger needs to call an HTTP API | @future(callout=true) or Queueable with Database.AllowsCallouts |
| Trigger needs to update unrelated records | After-insert/update Trigger directly (or @future to defer) |
| Need to process all 5M Accounts | Batch |
| Need to run something every Monday at 9 AM | Schedulable + Batch |
| Need to chain N async jobs | Queueable, enqueueing the next from execute() |
| Pub/sub events across systems | Platform Events |
Common exam triggers¶
- "Trigger needs HTTP callout" β @future(callout=true) or Queueable
- "Process 10 million records nightly" β Schedulable + Batch
- "Pass an sObject to async" β Queueable (future doesn't accept sObjects)
- "Chain jobs together" β Queueable with enqueueJob in execute
- "Real-time event from LWC to Apex" β Platform Event
- "Decouple slow downstream from current save" β @future or Queueable
- "Track progress across batches" β
Database.Statefulinterface - "Run logic at exactly 2 AM daily" β Schedulable with CRON expression