9.1 @future Methods

Key Takeaways

  • @future methods must be static, return void, and accept only primitives, collections of primitives, or Id—never sObjects
  • Use @future(callout=true) when the method performs HTTP callouts; callouts are not allowed from future methods without that flag
  • Future work runs in a separate transaction after the originating transaction commits—ideal for mixed DML and deferred work
  • You cannot call a future method from another future method; for chaining and complex state prefer Queueable
  • Test asynchronous execution with Test.startTest() and Test.stopTest() so future methods run inside the test boundary
Last updated: August 2026

9.1 @future Methods

Quick Answer: An @future method is a static void Apex method that the platform queues to run asynchronously in its own transaction after the current one commits. Parameters may be primitives, collections of primitives, or Id only—not sObjects. Use @future(callout=true) for HTTP callouts. Prefer Queueable when you need sObject parameters, job IDs, or chaining.

Asynchronous Apex is a core Process Automation and Logic skill on Platform Developer I. Triggers and controllers run in tight synchronous transactions with governor limits. When you must defer work—callouts, mixed DML, or heavy follow-up processing—@future is the classic tool. The exam still tests future rules even though Queueable is often preferred in new code.

Why Asynchronous Execution Exists

Synchronous Apex shares one transaction boundary: all SOQL, DML, and CPU count against the same limits, and the user waits for the response. Asynchronous paths:

  • Run after the originating transaction succeeds (when queued from that transaction)
  • Get their own governor limit buckets
  • Free the user from waiting on long external calls or bulk follow-up work

@future is one of four primary async mechanisms (future, queueable, batch, scheduled). This section focuses on future methods; later sections compare the others.

Syntax and Method Shape

public class AccountFutureService {
    @future
    public static void updateAccountNames(Set<Id> accountIds) {
        List<Account> accounts = [
            SELECT Id, Name FROM Account WHERE Id IN :accountIds
        ];
        for (Account a : accounts) {
            a.Name = a.Name + ' (Async)';
        }
        update accounts;
    }
}

Hard rules the exam loves:

RuleDetail
staticInstance methods cannot be @future
void returnNo return value to the caller
Parameter typesPrimitives (String, Integer, Boolean, …), arrays/collections of primitives, or Id / List<Id> / Set<Id>
No sObjectsCannot pass Account, List<Contact>, or other sObject types
No non-primitive complex typesNo custom Apex types that wrap sObjects or non-allowed types

Because you cannot pass sObjects, the standard pattern is: collect Ids in the trigger or controller, pass the Id collection into the future method, then re-query inside the future method for current field values.

// In a trigger handler (sync)
Set<Id> ids = new Set<Id>();
for (Account a : Trigger.new) {
    ids.add(a.Id);
}
AccountFutureService.updateAccountNames(ids);

Why no sObjects? Future methods run later. Passing a full sObject would freeze a stale in-memory snapshot that might no longer match the database, and serialization of complex graphs is restricted. Id + re-query is the intentional design.

@future(callout=true)

HTTP callouts (and some external services) are not allowed from future methods unless you annotate with callout support:

public class ExternalNotifyService {
    @future(callout=true)
    public static void notifyExternalSystem(Set<Id> caseIds) {
        List<Case> cases = [
            SELECT Id, CaseNumber, Subject FROM Case WHERE Id IN :caseIds
        ];
        HttpRequest req = new HttpRequest();
        req.setEndpoint('callout:My_Named_Credential/cases');
        req.setMethod('POST');
        req.setBody(JSON.serialize(cases));
        HttpResponse res = new Http().send(req);
        // handle res.getStatusCode() ...
    }
}

Without callout=true, a callout throws a runtime exception. You still cannot mix callouts and certain DML patterns carelessly; design the future method so callouts and DML order respect platform rules (often callout first, then DML of results, or structure work carefully).

Callouts cannot run in the same transaction as uncommitted DML in many sync paths—a frequent reason to move callouts into @future or Queueable after DML commits.

When Future Methods Shine

1. Mixed DML (setup + non-setup objects)

You cannot perform DML on setup objects (for example, User, Group, certain permission-related objects) and non-setup objects (for example, Account) in the same synchronous transaction in many combinations. A common fix is to move one side of the DML into a future method so it runs in a separate transaction:

// Sync: insert Account
insert new Account(Name = 'Acme');
// Async: update User in a separate transaction
UserFutureService.deactivateUser(userId);

2. Post-commit side effects after trigger DML

Triggers often need to notify an external system or recalculate related data without blocking the save. Enqueue future work with Ids; the future transaction runs after commit so external systems see committed data.

3. Avoiding governor pressure in the user transaction

Heavy SOQL/DML that is not required for the immediate save can be deferred so the interactive transaction stays under CPU, SOQL, and heap limits.

Limits and Constraints You Must Remember

Exact numeric limits can shift with releases; exam items emphasize behavior more than memorizing every number. Know these concepts:

  • A single transaction can enqueue only a limited number of future method invocations (platform future call limit).
  • Future methods count toward async execution capacity for the org; mass enqueue from bulk triggers must still be bulk-safe (one future call with a Set of Ids, not one future call per record).
  • No future-to-future chaining: a method already running as @future cannot invoke another @future method. That is a hard restriction and a classic exam trap.
  • Future methods do not return a job Id to the caller in the same convenient way Queueable does with System.enqueueJob.
  • Execution order among multiple futures is not guaranteed relative to each other; do not assume FIFO business sequencing without orchestration design.

Bulk anti-pattern:

// BAD: future call inside per-record loop
for (Contact c : Trigger.new) {
    ContactFuture.doWork(c.Id); // burns future invocations; may hit limits
}
// GOOD: one future with all Ids
ContactFuture.doWork(contactIds);

Future vs Queueable (Decision Frame)

NeedPrefer
Pass sObjects or complex stateQueueable
Chain job B after job A finishesQueueable (or Batch finish)
Receive and store a job IdQueueable (enqueueJob returns Id)
Simple Id-only deferred work / mixed DML@future still valid
Callouts with richer stateQueueable implementing Database.AllowsCallouts often cleaner
Legacy codebasesFuture still appears on exams and in older orgs

If the question shows sObject parameters or chaining, @future is wrong. If the question shows static void + Set of Ids + callout=true, future is in play.

Testing @future Methods

Asynchronous code does not run immediately when invoked in a test—unless you force it with the test boundary methods:

@IsTest
private class AccountFutureServiceTest {
    @IsTest
    static void updatesNamesAsync() {
        Account a = new Account(Name = 'Test Co');
        insert a;

        Test.startTest();
        AccountFutureService.updateAccountNames(new Set<Id>{ a.Id });
        // Future is queued here but runs when stopTest executes
        Test.stopTest();

        Account refreshed = [SELECT Name FROM Account WHERE Id = :a.Id];
        System.assert(refreshed.Name.contains('Async'));
    }
}

Rules of thumb:

  • Call the future method between Test.startTest() and Test.stopTest().
  • Assert after Test.stopTest(), when async work has been forced to complete.
  • For callout futures, combine with Test.setMock(HttpCalloutMock.class, mock) before the callout path runs (typically before or inside the start/stop window as appropriate).
  • Without start/stop, your asserts often see pre-async state and flaky or failing tests.

Test.startTest() also resets many governor limit counters for the code that runs until stopTest(), which helps tests that enqueue async work after significant setup DML.

Putting It Together for the Exam

When a snippet uses @future, scan for:

  1. Is the method static and void?
  2. Are parameters only primitives / Ids / collections of those?
  3. If there is a callout, is callout=true present?
  4. Is future invoked from another future? (illegal)
  5. Is it bulkified (one call, many Ids)?
  6. Would Queueable be a better answer for sObjects, chaining, or job monitoring?

Master these constraints and you will clear most future-method items quickly, leaving time for Queueable, Batch, and Scheduled orchestration questions next.

Test Your Knowledge

Which method signature is valid for an @future method that updates records by Id?

A
B
C
D
Test Your Knowledge

A developer needs an @future method to send an HTTP callout after a Case is inserted. What is required?

A
B
C
D
Test Your Knowledge

In an Apex unit test, when should assertions about records modified by an @future method typically run?

A
B
C
D