Designing Sync and Async Apex: When Operations Must Run in Sequence
A fundamental Salesforce development decision is whether a given operation should run synchronously (within the current transaction) or asynchronously (in a separate transaction via Queueable, @future, or Batch). This choice affects data consistency, user experience, governor limits, and automation behavior. It is also a core PD1 exam topic.
Synchronous vs. Asynchronous Execution
Synchronous code runs inline with the user's action. The user waits for it to complete, and the entire operation succeeds or fails atomically.
Asynchronous code runs in a separate transaction after the current one commits. It has its own governor limits, its own DML context, and no guarantee of immediate execution.
| Characteristic | Synchronous | Asynchronous |
|---|---|---|
| User waits | Yes | No |
| CPU limit | 10,000 ms | 60,000 ms (Queueable/Batch) |
| SOQL limit | 100 queries | 200 queries |
| DML limit | 150 statements | 150 statements |
| Transaction | Same as triggering action | Separate transaction |
| Rollback | Rolls back with parent | Independent |
| Data visibility | Sees uncommitted changes | Only sees committed data |
The Salesforce Order of Execution
When a record is saved, Salesforce follows a specific sequence:
- Load original record (or initialize for new)
- Load new field values from the request
- Execute before triggers
- Run system validation rules
- Run custom validation rules
- Execute duplicate rules
- Save record to database (not yet committed)
- Execute after triggers
- Execute assignment rules, auto-response rules
- Execute workflow rules
- Execute escalation rules
- Execute record-triggered Flows
- Execute entitlement rules
- Commit to database
- Execute post-commit logic (async jobs, platform events, outbound messages)
Asynchronous jobs enqueued during steps 3 or 8 execute after step 14 -- they only run once the transaction has fully committed.
When to Use Synchronous Processing
Use synchronous execution when:
- The user needs immediate feedback. Field defaults, validation, error messages.
- Data must be consistent within the transaction. A child record must reference its just-created parent.
- Downstream automations depend on the result. A before trigger sets a field that a validation rule checks.
- Rollback is required on failure. If step B fails, step A should also roll back.
// Synchronous: Set default values before save
trigger ItemTrigger on Item__c (before insert) {
for (Item__c item : Trigger.new) {
if (item.Priority__c == null) {
item.Priority__c = 'Medium';
}
}
}
When to Use Asynchronous Processing
Use asynchronous execution when:
- The operation is expensive. Heavy calculations, many child records, external callouts.
- The operation requires a committed parent record. You cannot query a record's related data until the transaction commits.
- The operation touches a different object context. Updating an Opportunity based on its Quote requires a separate transaction to avoid mixed-DML or recursive trigger issues.
- Failure should not block the user. A notification email failing should not prevent a record save.
// Asynchronous: Sync Quote totals to Opportunity after commit
public class QuoteSyncJob implements Queueable {
private Id quoteId;
public QuoteSyncJob(Id quoteId) {
this.quoteId = quoteId;
}
public void execute(QueueableContext ctx) {
Quote q = [SELECT Id, TotalPrice, OpportunityId FROM Quote WHERE Id = :quoteId];
Opportunity opp = [SELECT Id, Amount FROM Opportunity WHERE Id = :q.OpportunityId];
opp.Amount = q.TotalPrice;
update opp;
}
}
Decision Framework
Use this decision flow when determining sync vs. async:
Does the user need to see the result immediately?
YES -> Synchronous
NO -> Continue
Will the operation exceed CPU/SOQL limits if run inline?
YES -> Asynchronous
NO -> Continue
Does the operation depend on committed data from this transaction?
YES -> Asynchronous (must wait for commit)
NO -> Continue
Should failure of this operation block the entire save?
YES -> Synchronous
NO -> Asynchronous
Does the operation involve a callout to an external system?
YES -> Asynchronous (@future(callout=true) or Queueable)
NO -> Synchronous (if none of the above apply)
Common Pitfall: DML Before Async Enqueue
A Queueable enqueued during a trigger only executes after the transaction commits. If the transaction rolls back (due to a validation rule failure later in the order of execution), the Queueable is discarded -- it never runs. This is correct behavior, but developers sometimes expect the Queueable to execute regardless.
Choosing the Right Async Mechanism
| Mechanism | Best For | Chaining | Callouts |
|---|---|---|---|
@future | Simple, fire-and-forget operations | No | Yes (with callout=true) |
Queueable | Complex logic, needs instance state | Yes (depth 1 in triggers) | Yes |
Batch Apex | Processing thousands/millions of records | Via finish() | Yes |
Schedulable | Time-based recurring jobs | Via Batch or Queueable | No (directly) |
Understanding these trade-offs is essential for building scalable Salesforce applications and is heavily tested on the Platform Developer I exam.