@future annotation is the original "fire-and-forget" asynchronous tool in Salesforce. You tag a static method, and the platform runs it later. However, its strict limitations—only accepting primitive data types, no ability to chain jobs, and no traceable Job ID—are the exact reasons Queueable Apex was created to replace it. Today, @future is mainly used as a quick escape hatch for Mixed-DML errors.
@future(callout=true) to push data to an external ERP. It works fine until a developer tries to trigger that same logic from a Batch Apex job, resulting in the dreaded error: "future method cannot be called from a future or batch method."The Fix: The modern solution is to rewrite that logic as a
Queueable class, leaving @future purely for resolving quick Setup vs. Non-Setup object conflicts (Mixed-DML).
After a
Delivery__c record is saved, the system must call an SMS gateway. Because callouts cannot run synchronously inside a trigger, a developer decides to use @future.
- The Bad Way: The developer wants to pass the whole record into the method. The compiler refuses, stating
@futureonly accepts primitives. To bypass this, the developer serializes the record into a JSON string and passes that string instead. - The Pain: The job might wait in the queue for a few minutes before executing. During that time, the JSON string remains a stale snapshot. When the job finally runs and updates Salesforce, it silently overwrites any new status updates a driver made in the meantime.
- The Good Way: Pass only the record ID (or a
Set<Id>). Inside the asynchronous method, re-query the database for the current record. Make the callout using fresh, up-to-date data. - The Payoff: The restriction against passing sObjects isn't just an annoying rule—it is a built-in guardrail designed to force you to use fresh data. If you need complex objects, chaining, or Job IDs, use
Queueable(and you should still re-query your data there!).
The Core Concept of @future Methods
The @future annotation flags a standard static void method to execute asynchronously in the background. However, it comes with strict parameters:
- No sObjects Allowed: You can only pass primitive data types (like Strings or Integers) or collections of primitives. Because the record might change before the background job runs, you must pass Ids and re-query the data.
- Callouts: You must explicitly add
(callout=true)if the method needs to hit an external API. - No Chaining or Tracking: You cannot chain another job from a future method, nor do you get a tangible Job ID object to monitor in your code (beyond checking standard
AsyncApexJoblogs). - Context Restrictions: You cannot call a future method from Batch Apex, Scheduled Apex, or another future method.
- Rule: Do not write new
@futuremethods.Queueabledoes everything it does, and much more. - Gain: It is the absolute simplest form of asynchronous Apex—just annotate a static method and walk away. Fine for legacy fire-and-forget callouts.
- Price: Every limitation costs you. No sObjects, no chaining, no job ID, which means no custom monitoring and no easy retry logic.
- Limits: You are capped at 50
@futurecalls per transaction. It cannot be executed from Batch, Scheduled, or other future contexts. - Mirror — Queueable: Accepts complex objects in, returns a Job ID out, supports job chaining, and allows Transaction Finalizers for clean failure handling.
- Modern Use Case: The only real job
@futurestill holds onto is splitting DML operations between Setup and Non-Setup objects (Mixed-DML errors). - At Volume: Having no Job ID means you have no programmatic way to check how far a massive data load's async work has progressed.
@future method is a classic trap. Because the job runs minutes later, that snapshot becomes stale. When the job performs its DML, it silently overwrites any newer data. Always pass Ids and re-query.
Core Q&A
A: Async-from-async is strictly blocked for future methods. You cannot invoke it from a Batch's execute() or finish() method. Instead, you should enqueue a Queueable job. Queueables can be started from a batch context (one enqueue per execute() method) or chained directly from a finish() method. Interviewers ask this to see if you actually understand the asynchronous matrix, or if you just memorized syntax.
Scenario-Based Follow-Ups
A1: Because asynchronous execution is deferred. If Salesforce allowed you to pass an sObject, the data held in memory at enqueue time would become a stale snapshot by the time the method actually runs. If the future method updates the record, it would blindly overwrite any changes made by users or other automation in the interim. Passing an Id forces the platform into a safe pattern: the method is forced to re-query the current state of the database at run time.
A2: Classify every instance based on its risk and complexity.
- Simple, callout-only, fire-and-forget futures can stay. They are low risk.
- Anything requiring error handling, chaining, or complex state management must be migrated to
Queueable, complete with Transaction Finalizers for guaranteed logging. - Evaluate Mixed-DML workarounds to ensure they still make sense in the current data model.
- Check for duplicate suppression:
@futuredoes not deduplicate jobs. Trigger-driven futures often blindly stack 100 identical jobs in a single transaction. Fix this by collecting Ids into a staticSetand enqueuing the work exactly once at the end of the transaction. - Check Limits: A limit of 50 future calls per transaction sounds generous until a bulk data load multiplies it instantly.
A3: Almost none. Queueable does everything @future does, while adding object parameters, job chaining, Job IDs, and Finalizer support. All new asynchronous code should default to Queueable. You keep your knowledge of @future sharp for two reasons only: fixing legacy code, and answering this exact interview question.