Skip to main content

Salesforce @future Methods vs. Queueable: The Modern Guide

๐Ÿ’ฌ In plain words: The @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.
๐Ÿ“Œ The Problem: In legacy code, a trigger might call @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).
๐ŸŽฌ Real-Life Example: The Stale Snapshot
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 @future only 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!).
๐Ÿง  Core Rule: Fire and forget. @future only takes primitives, cannot be chained, and provides no Job ID. Every limitation it has is a reason Queueable exists.

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 AsyncApexJob logs).
  • Context Restrictions: You cannot call a future method from Batch Apex, Scheduled Apex, or another future method.
๐Ÿงญ 360 Card — Future Methods
  • Rule: Do not write new @future methods. Queueable does 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 @future calls 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 @future still 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.
⚠ INTERVIEW TRAP: Serializing a record into a JSON string just to pass it into a @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

Q: Can you call a @future method from Batch Apex? From another @future? What is the correct pattern instead?
๐ŸŽฏ Say this first: "No, and no. A @future method cannot be called from a batch or from another @future method. The correct architectural pattern is Queueable Apex, which allows job chaining and accepts complex objects."

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

Q1: Why do @future methods only accept primitives, and what bug does passing an Id defend against?

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.

Q2: You are auditing a legacy org full of @future calls. What is your modernization checklist?

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: @future does not deduplicate jobs. Trigger-driven futures often blindly stack 100 identical jobs in a single transaction. Fix this by collecting Ids into a static Set and 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.
Q3: @future vs Queueable — is there any valid reason left to write a brand new @future method?

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.