Async design is mostly choosing the right pattern. The implementation is straightforward once the choice is made; the failures come from forcing one pattern when another fits better.
- Identify what cannot run synchronously
Callouts from triggers, processing more than a few thousand records, long-running calculations, anything the UI thread should not wait for. These are async candidates.
- Pick the pattern
Trigger callout: Future or Queueable. Heavy one-shot: Queueable. Bulk data: Batch. Recurring: Scheduled Apex submitting Batch. Document the choice; future maintainers benefit from the reasoning.
- Implement with sane error handling
Wrap risky operations in try/catch. Log failures to a custom error object so they survive past Apex Jobs retention. Async failures are easy to miss without explicit logging.
- Test against representative data
Async jobs that pass in a 10-row dev org can fail at scale. Run tests against full-volume sandboxes with realistic data shapes before promoting to production.
- Monitor in production
Build a dashboard on AsyncApexJob. Watch for failure spikes, long-running jobs, and queue saturation. Catch issues before users do.
- Future methods cannot accept complex sObject parameters; they accept only primitives and primitive collections. Queueable supports any sObject.
- Async limits are larger than sync but still real. A Queueable that touches 100,000 records still needs careful bulkification.
- Async failures are easy to miss without explicit logging. AsyncApexJob retention is weeks; persistent logging requires a custom object.
- Chaining queueable jobs has a depth limit. Production patterns use chunked submission with explicit IDs to avoid running into the chain limit.