Apex Trigger for Production in Salesforce: A Best Practice Guide

Hasan Sajedi
8 min readNov 5, 2024

--

A Salesforce trigger is a powerful tool that enables developers to automate actions before or after events such as record insertions, updates, or deletions. Triggers are crucial in enforcing business logic at the database level, maintaining data consistency, and providing a seamless user experience. When designed for production environments, triggers need to be reliable, efficient, and error-resistant to handle large data volumes and complex workflows.

This article will discuss when and why we might need a custom trigger in Salesforce, best practices for building one, and showcase an example structure for error handling and optimized processing. By following these techniques, you’ll create a robust trigger that ensures reliable performance under production conditions.

Why Use a Custom Trigger?

Triggers allow developers to:

  1. Enforce specific business rules at the database level.
  2. Automate complex data processing tasks.
  3. Ensure data consistency across different objects or fields.
  4. Execute actions in response to DML events (insert, update, delete, etc.).

Salesforce triggers can be implemented in two modes:

  1. Before triggers: Execute before a record is saved to the database. Useful for validation or setting default values.
  2. After triggers: Execute after a record has been saved. Useful for complex processing that requires the record ID, such as creating related records.

Options for Implementing a Trigger

When implementing triggers, especially on a production platform, you have various options for handling record processing:

  1. Synchronous Processing: Executes immediately, allowing you to see the result right away.
  2. Asynchronous Processing: Executes in the background, useful for operations that don’t need to complete instantly (e.g., Future, Queueable, or Batch Apex).

Each of these options has different implications on system limits and processing times, making it essential to choose the appropriate approach depending on the context of the trigger.

Limitations of Asynchronous Processing in Salesforce: Future, Queueable, and Batch Apex

When building scalable and efficient Salesforce applications, asynchronous processing methods like @future, Queueable, and Batch Apex are indispensable. However, each method comes with specific limitations and constraints that impact when and how they should be used. Understanding these limitations helps developers choose the right solution for their use cases and avoid unexpected errors in production environments.

1. Future Methods

  • No Chaining or Complex Job Orchestration: Future methods cannot be chained, meaning you can’t enqueue another future method within a future method. This limits the ability to build dependent, multi-step processing flows.
  • Limited Parameter Types: Future methods only accept primitive data types or collections of primitives (e.g., String, List<String>, Set<Id>). Complex data types, like sObjects or custom objects, cannot be directly passed.
  • No Guaranteed Order of Execution: Multiple future methods can run out of order. If order is important for data consistency, future methods may not be ideal.
  • No Access to Certain Trigger Context Variables: In future methods, some context, such as Trigger.old and Trigger.new, becomes inaccessible, making it difficult to work directly with data from trigger execution.
  • Governor Limit: Salesforce limits the platform to 50 future calls per transaction. Surpassing this limit will raise an error, often leading to failed transactions.

2. Queueable Apex

  • Limited Chaining: Queueable Apex supports chaining, allowing for more complex processing flows than future methods. However, chaining is limited to one level deep — meaning that only one Queueable job can be added by another Queueable job. Any further nesting is not allowed.
  • Single Queued Job Limit in Triggers: Within a trigger, you are limited to only one Queueable job per transaction. Exceeding this can result in a System.LimitException.
  • Potential Delays Due to Queue Backlog: Queueable jobs depend on the system queue. When many jobs are queued, jobs can face delays in execution, making Queueable Apex less suitable for time-sensitive processing.
  • Governor Limit: Salesforce enforces a limit of 50 Queueable jobs per transaction. As with future methods, breaching this limit causes errors, so careful batch design is necessary when processing large volumes.

3. Batch Apex

  • Complexity and Setup: Batch Apex is ideal for processing very large data volumes and handling long-running operations, but it requires a more complex setup, including implementing methods like start, execute, and finish.
  • Limited Control Over Execution Timing: Although developers can specify a preferred start time for batch jobs, they can’t control exact timing. The job may run sooner or later based on system load, which can impact time-sensitive tasks.
  • Governance Limits on Concurrent Jobs: Batch Apex jobs are limited to five queued or active batch jobs per org. In cases where higher concurrency is required, developers may hit this limit, resulting in job queuing or delayed execution.
  • Limited Depth in Chaining: While Batch Apex supports chaining, chaining depth is restricted by Salesforce governor limits and timing constraints. Excessive chaining may lead to errors, especially if jobs exceed execution limits or hit other system constraints.
  • Resource Intensive: Batch jobs are more resource-intensive, as they create separate transactions for each batch. Each execution includes initialization, context setup, and commit actions, which can consume significant processing time and resources, particularly in high-demand environments.

Each of these asynchronous methods has particular use cases, and their limitations can guide your selection process. For scenarios that involve processing high data volumes without strict time requirements, Batch Apex can be a good fit. If you need simpler, one-time asynchronous processing with more flexible data types, Queueable is often a solid choice. When you have lightweight asynchronous tasks with minimal data requirements, future methods are useful but need careful handling to avoid issues with parameter restrictions and governor limits.

By selecting the appropriate method and carefully architecting your asynchronous logic, you can maximize reliability and performance while minimizing errors and governor exceptions in your Salesforce application.

Code Walkthrough

Let’s dive into the following structure and analyze its significance for building a production-grade trigger:

if (Trigger.isExecuting) {
if (!System.isFuture() && !System.isBatch()) {
callFutureMethod(...);
} else {
callQueueableClass(...);
}
} else {
if (!System.isFuture() && !System.isBatch()) {
callSynchronouslyMethod(...);
} else {
callQueueableClass(...);
}
}

This code structure is designed to improve trigger efficiency, ensure proper error handling, and prevent system limit issues in high-demand production environments.

  • Condition: Trigger.isExecuting

This checks if the trigger is currently in an active execution context. It’s crucial because it ensures the logic is only applied while the trigger is actually running.

  • Condition: !System.isFuture() && !System.isBatch()

The system checks if the trigger is neither in a Future context nor a Batch context. The purpose here is to identify when the trigger should run operations in the background using a Future method. This choice is valuable for handling large data volumes since it moves non-essential work out of the synchronous transaction.

  • Using sendAccountsFuture(accountIds)

This method call, annotated with @future(callout=true), allows the process to run asynchronously, enabling us to execute HTTP callouts or any long-running operations without impacting the main thread. This method is particularly effective for maintaining limits in Salesforce, where synchronous calls are limited.

  • Fallback to sendAccountsQueueable(accountIds)

If the code is already in a Future or Batch context, it falls back to a Queueable job (sendAccountsQueueable). Queueable Apex offers more control than @future, such as handling chained jobs, which can be vital for complex processing.

  • Else Block: Non-Trigger Context Execution

The else block handles cases when the code isn’t executing within the primary trigger context, allowing synchronous processing through sendAccountsSynchronously(accountIds). This is crucial for tests or situations where trigger context isn’t present, preventing unnecessary queuing and making the code more adaptable.

Importance of This Structure

This structure offers multiple benefits:

  • Error Handling: By managing asynchronous contexts explicitly, this structure prevents LimitException errors, like “Too many queueable jobs added to the queue,” which can occur when multiple jobs stack up in the same context.
  • System Limit Compliance: The approach of alternating between Future and Queueable ensures the solution stays within Salesforce governor limits, crucial in production environments with high data volumes.
  • Optimized Performance: Moving certain actions to a queue or future context prevents slowdowns in user experience by allowing processing-intensive actions to complete in the background.

Trigger Code Example: Handling changes on Case objects

The following code snippet demonstrates a structure that ensures the trigger behaves reliably in different contexts (synchronous, asynchronous, batch). It makes use of asynchronous methods (@Future and Queueable) for background processing when needed, reducing system load and avoiding governor limits.

trigger CaseObjectTrigger on Case (after delete, after update) {
if (Trigger.isDelete) {
CaseTriggerHandler.handleAfterDelete(Trigger.old);
}

if (Trigger.isUpdate) {
CaseTriggerHandler.handleAfterUpdate(Trigger.new, Trigger.oldMap);
}
}

Key Code Structure: Handling Synchronous vs. Asynchronous Processing

Below is a code structure demonstrating how to choose between synchronous and asynchronous methods based on trigger context:

if (Trigger.isExecuting) {
if (!System.isFuture() && !System.isBatch()) {
CaseHandlers.processCasesFuture(caseIds);
} else {
CaseHandlers.enqueueCaseProcessing(caseIds);
}
} else {
if (!System.isFuture() && !System.isBatch()) {
CaseHandlers.processCasesSynchronously(caseIds);
} else {
CaseHandlers.enqueueCaseProcessing(caseIds);
}
}

In this setup:

  • Synchronous Processing (processCasesSynchronously): If neither System.isFuture() nor System.isBatch() is true, we process cases synchronously. This is ideal for simple updates that need immediate feedback but should be used sparingly to avoid system limits.
  • Future Methods (processCasesFuture): When asynchronous processing is needed but without batch processing, @Future methods allow non-blocking processing and can make callouts to external systems if needed. It’s crucial for operations that might exceed normal limits or need to wait for external resources.
  • Queueable Methods (enqueueCaseProcessing): For more complex background processing, such as handling large data volumes, Queueable Apex provides greater flexibility. Queueable jobs are similar to batch jobs but are more flexible, allowing complex, chainable operations in a background queue.

Related Methods in CaseHandlers

Each method referenced in the above structure is designed for different processing needs:

public class CaseHandler {
@future
public static void processCasesFuture(List<String> caseIds) {
// Future method code for processing cases
}

public static void enqueueCaseProcessing(List<String> caseIds) {
System.enqueueJob(new CaseProcessingQueueable(caseIds));
}

public static void processCasesSynchronously(List<String> caseIds) {
// Synchronous code for immediate case processing
}
}

CaseActionQueueable Class for Asynchronous Case Processing

The CaseActionQueueable class defines a queueable job that processes cases in the background. This class is beneficial for handling complex workflows that can be chained or reprocessed without affecting the main transaction.

public class CaseActionQueueable implements Queueable, Database.AllowsCallouts {
private List<String> caseIds;

public CaseActionQueueable(List<String> caseIds) {
this.caseIds = caseIds;
}

public void execute(QueueableContext context) {
// Logic to process cases in queueable context
updateCases(caseIds);
}

private void updateCases(List<String> caseIds) {
// Code for updating cases
}
}

Conclusion

Building a custom trigger for production use in Salesforce requires not only technical implementation but also strategic error management and efficient processing techniques. By using a structured approach like the one above, you can avoid common pitfalls related to system limits, ensure smooth operations in high-demand scenarios, and deliver a reliable trigger that meets production standards.

Resources

  1. Salesforce Triggers Overview
    Salesforce Triggers Overview
    This guide explains the basics of triggers, trigger contexts, and examples of how to write them for specific use cases.
  2. Apex Developer Guide
    Apex Developer Guide
    An extensive guide on Apex development, including trigger syntax, best practices, and error handling.
  3. Queueable Apex
    Queueable Apex
    This guide covers the Queueable Apex interface, which allows asynchronous execution with better control over job chaining and allows for handling larger data volumes.
  4. Future Methods
    Future Methods
    An explanation of @future methods, which are helpful for asynchronous processing and making callouts in contexts where it’s normally restricted.

Tutorials and Blog Posts

  1. https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_triggers.htm
  2. https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_intro.htm
  3. https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_queueing_jobs.htm
  4. https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_classes_annotation_future.htm
  5. https://trailhead.salesforce.com/en/content/learn/modules/apex_triggers

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

--

--

No responses yet

Write a response