Functions Patterns and Best Practices

This section has general development patterns and best practices for Functions developers.

Patterns for Apex Developers

If you're an Apex developer you may have complex business logic implemented in Apex that you’d like to move to a Salesforce Function. Moving your code to a Function lets you take advantage of the elastic Salesforce Functions compute environments, so you no longer have to worry about Apex governor limits like CPU time.

However, since Functions code runs “outside” of your org, certain things like transactions and data access work differently from Apex code running in your org. Take into account the following development patterns when converting your Apex code into Functions.

Manage Transactions in Your Function

Functions run outside the Apex transaction from which they were invoked. Data changes made in a Function aren’t automatically tracked as part of a transaction, and therefore can’t be rolled back as part of an Apex transaction. Similarly, if your Function crashes before committing all data changes, partial changes are not automatically rolled back, and uncommitted changes are not automatically tracked. If you need transactions within your Function, you’ll need to manage transactions yourself.

To manage transactions in a Function, use the UnitOfWork SDK class. UnitOfWork can be used to add data operations to a transaction that can be committed as a single operation. Use the register methods, such as registerCreate() or registeredDelete(), to register data operations and then commitUnitOfWork() to commit the set of operations. Until you commit, nothing is changed in your org’s data.

As an example, suppose you had the following Apex code in a class method:

This method would be treated as one transaction, so if any exception or error occurred between inserts, all data changes made in this method would get automatically rolled back.

If you convert this Apex code into JavaScript using several calls, you run the risk of not being able to roll back data changes if an error occurs. To properly convert the whole Apex transaction into JavaScript, you’d use UnitOfWork to encapsulate the transaction:

Take Advantage of Higher Compute Limits

Because Functions run outside your org in Salesforce Functions compute environments, Function code can be run without worrying about Apex compute limits. Your Apex code might have been designed to take into account things like Apex governor CPU limits, so you may be able to improve the efficiency and manageability of your code when you port to Salesforce Functions.

For example, you might have opted to use Apex Maps to pre-fetch and pre-process query results to reduce CPU use. In your Function, you may be able to avoid this pre-process step. Similarly, if you were hitting CPU limits while doing things like date/time comparisons across large data sets, you’ll be able to do the same set of comparisons in a Function without hitting a CPU limit.

Note that you should generally still consider following best practices around making your queries and DML more efficient, as data operations from Functions are governed by API call limits, not CPU limits.

Avoid Excessive Data API Calls

Currently API requests from Functions are counted against the maximum API requests for a given org, however this applies to the Functions beta only and may change once Functions is generally available.

Unlike Apex, Functions need to use Salesforce Web Service API calls to access org data. There are various API call limits, such as the number of API calls made during a 24 hour period, and record size limits. For the Functions beta, Functions currently need to honor these API limits. Your Apex code likely wasn’t written with these limits in mind, so consider using the following approaches to avoid excessive API calls in your Function:

  • Use UnitOfWork. Along with transaction support, UnitOfWork uses the Composite Graph API to batch together several operations into a single API call.
  • Depending on your transaction needs, consider doing data changes in Apex rather than in the Function, possibly passing information about records that should be changed back from the Function to Apex in the Function response data. This lets you do the actual data operations in the Apex code that originally invoked your Function.
  • In some scenarios you might be able to do some data operations in Apex prior to invoking the Function and, if needed, pass the results of these operations to the Function via the Function invoke payload. Keep in mind you should keep your payload size to 6MB or less (12MB for asynchronous invocations).
  • For very large data operations that can be performed asynchronously, consider using the Bulk API 2.0 from your Function. The Function payload provides a Salesforce API token for the invoking org that you can use when making HTTP REST API calls to Salesforce APIs like Bulk API 2.0.

It also helps to monitor API usage of your Function. Salesforce provides various APIs and tools to check current API usage. For more information on API limits and how to monitor API usage, see Monitoring Your API Usage.

See Limits for more details on API limits and other comparable limits when working with Functions.

Use Functions Permission Set

Your Apex code typically runs as the current user, in Apex system mode, and can have somewhat broad object permissions, as described in Understanding Describe Information Permissions. Your Function code, on the other hand, runs outside of your org, and therefore has different permissions. Deployed Functions use the "Functions" permission set to determine object access.

If you're converting your Apex code into Function code, you may have to review what object permissions your Function code needs, and update the "Functions" permission set accordingly.

For more details on Functions permissions, see Function Permissions

General Best Practices

Keep the following best practices in mind while developing Functions.

Bulkify Functions and Payloads When Batch Processing

If you are using your Function to do batch processing of your org data, consider bulkifying the work within the Function itself. For example, instead of invoking a Function to process a single record (and then invoking that Function multiple times, possibly in a loop, from Apex), consider passing a set of record IDs that the Function can work on. You can send up to a 6MB payload (12MB for async invocation) per invocation, so you can use that payload limit to send a batch of IDs, or even a SOQL query string that the Function can use to get the set of records to operate on.

Do Not Rely on Function State

Salesforce Functions are run in a stateless model. Nothing is automatically persisted from invocation to invocation, so your Function should not make any assumptions about system caches or filesystem content. Similarly, your Function should not rely on information generated in a previous run. If your Function needs to persist state or information between invocations, you’ll need to persist and manage this information in a separate data store you control (this could be data in your org, such as a custom object or Apex platform cache). If your Function needs to have an initial state of variables at run time, set that state in your Function code that gets run during Function invocation, and not in a global variable or external package.

You should not make any assumptions on the run time of an invocation. Salesforce Functions will launch and run your Function as efficiently as possible, but the run time for a Function is not guaranteed to be the same for every invocation. It is even possible for Functions to finish “out of order” — for example, depending on the complexity and dependencies of a Function container, an initial run of a Function may take longer than subsequent runs, and therefore a second run of a Function might finish before the first run.