Study guide

9.1 @future Methods

Salesforce Certified Platform Developer I Exam

Track your progress as you read

Finished sections get a check mark.

1 Introduction & Exam Overview

1.1 Current Exam Facts 1.2 Blueprint Weights & Study Strategy

2 Platform Fundamentals

2.1 Multi-Tenant Architecture & Shared Resources 2.2 MVC Mapping on the Lightning Platform 2.3 Declarative vs Programmatic Decisions 2.4 Agentforce for Developers: Use Cases & Limitations

3 Data Modeling & Schema

3.1 Objects, Fields & Schema Design 3.2 Relationship Types & Implications 3.3 Schema Design Impact on Apex 3.4 Data Import & Export for Development

4 Platform Data Features

4.1 Formula Fields & Roll-Up Summaries 4.2 Core CRM Objects & Standard Schema 4.3 AppExchange Extensions & Platform Boundaries

5 Declarative Process Automation

5.1 Record-Triggered Flows & Declarative Automation 5.2 Approval Processes & Business Rules 5.3 When to Use Apex vs Declarative Tools

6 Apex Language Basics

6.1 Variables, Constants & Apex Data Types 6.2 Lists, Sets & Maps 6.3 Control Flow & Exception Handling 6.4 Classes, Interfaces & OOP Patterns

7 SOQL, SOSL & DML

7.1 SOQL Query Fundamentals 7.2 SOSL Search Fundamentals 7.3 DML Operations & Bulkification 7.4 Governor Limits & Transaction Boundaries

8 Apex Triggers & Order of Execution

8.1 Trigger Structure & Context Variables 8.2 Bulk Trigger Patterns & Best Practices 8.3 Save Order of Execution 8.4 Recursion, Cascading & Transaction Control

9 Asynchronous Apex

9.1 @future Methods 9.2 Queueable Apex 9.3 Batch Apex 9.4 Schedulable Apex & Job Orchestration

10 Security, Events & Schema Access

10.1 Sharing, FLS, CRUD & Injection Prevention 10.2 Platform Events Publish & Subscribe 10.3 Programmatic Schema Access (Describe)

11 Visualforce

11.1 Visualforce Pages & Markup 11.2 Standard, Custom Controllers & Extensions 11.3 Visualforce in Lightning Experience

12 Lightning Components

12.1 Lightning Component Framework Benefits 12.2 Aura Component Structure & Resources 12.3 Lightning Web Components Fundamentals 12.4 LWC Data Access: Wire, Imperative & Events

13 UI Composition & Apex Integration Patterns

13.1 Composing UI in Lightning Apps 13.2 UI Security, Performance & Accessibility Patterns 13.3 Apex Actions for Flow, LWC, and Agentforce

14 Apex Testing

14.1 Testing Framework & Coverage Requirements 14.2 Writing Unit Tests for Triggers & Classes 14.3 Test Data Strategies & Isolation 14.4 Executing Tests & Execute Anonymous Differences

15 Debugging & Developer Tools

15.1 Debug Logs & Monitoring 15.2 Developer Console, CLI, VS Code & Workbench 15.3 Sandboxes, Scratch Orgs & Environment Strategy

16 Deployment & Release

16.1 Change Sets & Metadata Deployment 16.2 Unlocked Packages, Metadata API & Salesforce DX 16.3 Release Readiness & Credential Maintenance

9.1 @future Methods

Key Takeaways Explain This Topic AI

@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:

  • Rule Detail
  • static Instance methods cannot be @future
  • void return No return value to the caller

Parameter types Primitives ( String , Integer , Boolean , …), arrays/collections of primitives, or Id / List<Id> / Set<Id>

  • No sObjects Cannot pass Account , List<Contact> , or other sObject types
  • No non-primitive complex types No 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)
  • Need Prefer
  • Pass sObjects or complex state Queueable
  • Chain job B after job A finishes Queueable (or Batch finish)
  • Receive and store a job Id Queueable ( enqueueJob returns Id)
  • Simple Id-only deferred work / mixed DML @future still valid
  • Callouts with richer state Queueable implementing Database.AllowsCallouts often cleaner
  • Legacy codebases Future 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:

Is the method static and void ?

Are parameters only primitives / Ids / collections of those?

If there is a callout, is callout=true present?

Is future invoked from another future? (illegal)

Is it bulkified (one call, many Ids)?

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.