selah.net

Apex Triggers in Salesforce: Best Practices, Pitfalls, and Pro-Level Tips

Apex Triggers in Salesforce Best Practices Pitfalls and Pro Level Tips V1 1

What Are Apex Triggers in Salesforce?

Apex Triggers are like smart assistants in the background of Salesforce. Imagine you’re working with a list of contacts, and every time a new one is added or updated, something magical happens automatically—like checking for duplicates or assigning a sales rep. That magic? It’s powered by Apex Triggers.

Triggers are pieces of Apex code that run before or after records are added, changed, or deleted in the system. They help automate tasks that you’d otherwise have to do manually.


When and Why to Use Apex Triggers

You should use triggers when you need to go beyond basic automation. They’re especially useful when:

  • You want to update related records based on changes.
  • You need to apply business logic across multiple objects.
  • You’re dealing with complex operations that Flow or Process Builder can’t handle.

Analogy:
If you were baking cookies and needed to check the temperature before putting the dough in the oven, that’s a before trigger. If you needed to label them after they came out, that’s an after trigger.


Trigger vs. Process Builder and Flow – What to Use When

Salesforce gives you several tools for automation. Here’s when to use each:

  • Flow: Best for simple automations like sending emails or updating fields. Visual and easy to use.
  • Process Builder: Being phased out, but still useful for quick, rule-based automations.
  • Apex Triggers: Use these when things get complicated—like updating multiple records, enforcing cross-object logic, or working with large datasets.

TL;DR:

Use Flow when you can. Use Triggers when you must.


Types of Apex Triggers and Trigger Events

Triggers can be broken into two big types: before and after. These determine when the trigger code runs in relation to Salesforce saving the data.

Before vs. After Triggers

  • Before Triggers: Run before the record is saved. Use for formatting fields or setting default values.
  • After Triggers: Run after the record is saved. Use for creating or updating related records.

Examples:

  • Before Trigger: Add “+ Verified” to a Lead’s name.
  • After Trigger: Once a lead is created, create a related Task.

Trigger Events Explained

Each trigger type can respond to different events:

  • insert – Runs when a new record is created.
  • update – Runs when an existing record is changed.
  • delete – Runs before or after a record is deleted.
  • undelete – Runs when a record is restored from the Recycle Bin.

Example Syntax:

trigger ContactTrigger on Contact (before insert,before update,after insert,after update,before delete, after delete,after undelete) {

    // logic goes here

}

By understanding both trigger types and events, you’ll write cleaner code that responds at just the right time.


Trigger Execution Order – How Salesforce Handles It Behind the Scenes

When a record is created or updated in Salesforce, a lot happens behind the scenes. The platform follows a well-defined order of execution that determines how triggers, workflows, validation rules, and automation tools run. Knowing this order helps you avoid unexpected behavior or conflicts between automations.

Standard Order of Execution

Here’s a simplified version of what happens:

  1. System Validations – Salesforce checks field formats and required fields.
  2. Before Triggers – Your custom logic runs before the record is saved.
  3. Custom Validations – These are rules you’ve defined.
  4. Duplicate Rules – Salesforce checks for duplicates.
  5. Save Record to Database (but not committed yet)
  6. After Triggers – Logic that depends on the saved record runs here.
  7. Assignment Rules – These might assign leads or cases.
  8. Auto-Response Rules – Automatic emails or actions.
  9. Workflow Rules – Including field updates and outbound messages.
  10. Processes (Flow) – Modern Flow-based automation.
  11. Escalation Rules – For cases.
  12. Roll-Up Summary Fields – Get recalculated.
  13. Criteria-Based Sharing Rules – Get evaluated.
  14. Commit Record to Database – Finally, it’s saved permanently.
  15. Post-Commit Logic – Like sending emails or platform events.

Understanding this flow helps you avoid putting critical logic in the wrong place. For example, a field updated by a workflow after your trigger won’t be visible in your trigger unless you’re in an after context or use future methods.


Visualizing the Lifecycle

Sometimes, it’s easier to see the flow than read about it. You can picture it like a staircase:

  1. Validations
  2. Before Triggers
  3. Workflows & After Triggers
  4. Flows & Assignment Rules
  5. Save Record
  6. Post-Commit Actions

➡️ [Insert Lifecycle Flowchart or Animated GIF Here]

Pro tip: Salesforce’s developer docs have a document to reference.


Apex Trigger Syntax and Context Variables

Understanding trigger syntax and context variables is key to writing effective and scalable Apex code. Let’s break it down step-by-step.

Sample Syntax Template

Here’s a basic example of an Apex Trigger that runs before a new Contact is saved:

trigger ContactTrigger on Contact (before insert) {

    for (Contact con : Trigger.new) {

        if (con.Email != null && con.Email.endsWith(‘@example.com’)) {

            con.Description = ‘Internal Contact’;

        }

    }

}

This trigger checks each new Contact being inserted. If the email ends with “@example.com”, it sets the Description field to “Internal Contact.”


List of Context Variables

Context variables help your trigger know what’s happening and respond accordingly. Here are the most common ones:

  • Trigger.isInsert – True if it’s an insert operation
  • Trigger.isUpdate – True if it’s an update
  • Trigger.isDelete – True if it’s a delete
  • Trigger.isBefore – Runs before the record is saved
  • Trigger.isAfter – Runs after the record is saved
  • Trigger.new – The new version of the record
  • Trigger.old – The old version of the record (before changes)
  • Trigger.newMap – A map of new records by their IDs
  • Trigger.oldMap – A map of old records by their IDs

How to Use Context Variables Wisely

Using context variables correctly helps you avoid bugs, improve performance, and follow best practices:

  • Use Trigger.isInsert or Trigger.isUpdate to keep logic separated and focused.
  • Only access Trigger.old or Trigger.oldMap in update or delete triggers.
  • In after triggers, never try to modify Trigger.new records—they’re read-only.
  • Use newMap and oldMap for efficient comparison or bulk operations.

Example:

if (Trigger.isUpdate) {

    for (Account acc : Trigger.new) {

        Account oldAcc = Trigger.oldMap.get(acc.Id);

        if (acc.Name != oldAcc.Name) {

            System.debug(‘Account name changed from ‘ + oldAcc.Name + ‘ to ‘ + acc.Name);

        }

    }

}

This snippet compares old and new values for the Account Name and logs the change. It’s a safe and scalable way to manage updates in your triggers.


12 Apex Trigger Best Practices (With Code Samples)

1. One Trigger per Object

Only create one trigger for each object. This keeps your logic clean and manageable. Instead of scattering logic across multiple triggers, centralize it using a handler class.

trigger AccountTrigger on Account (before insert, before update) {

    AccountTriggerHandler.handle(Trigger.new, Trigger.old);

}


2. Keep Triggers Logic-Free (Use Handler Classes)

Move the actual business logic to separate Apex classes. Triggers should only pass data to these handler classes.

public class AccountTriggerHandler {

    public static void handle(List<Account> newList, List<Account> oldList) {

        // your business logic goes here

    }

}


3. Bulkify All Logic

Always assume your trigger is processing hundreds of records at once—even if you’re just testing with one. Avoid single-record operations.


4. Avoid SOQL/DML Inside Loops

This is a common mistake that causes you to hit governor limits. Move SOQL/DML queries outside of loops.

List<Contact> contactsToUpdate = new List<Contact>();

for (Account acc : Trigger.new) {

    // build list

}

update contactsToUpdate;


5. Use Collections Efficiently

Use Sets, Maps, and Lists for efficient lookups and bulk processing. They reduce redundancy and improve performance.

Set<Id> accountIds = new Set<Id>();

for (Contact c : Trigger.new) {

    accountIds.add(c.AccountId);

}


6. Query Only Required Fields

Don’t fetch all fields—just the ones you need. This reduces memory usage and improves performance.

SELECT Id, Name FROM Account WHERE Id IN :accountIds;


7. Implement Try-Catch Error Handling

Always wrap risky operations in a try-catch block to handle unexpected issues gracefully.

try {

    update myRecords;

} catch (Exception e) {

    System.debug(‘Error: ‘ + e.getMessage());

}


8. Use Custom Metadata Instead of Hardcoded IDs

Hardcoding values like RecordType IDs is risky and makes your code brittle. Use Custom Metadata or Custom Settings.


9. Build Context-Aware Handlers

Make your handler methods aware of trigger context. This lets you reuse methods across insert, update, and delete events.

if (Trigger.isInsert) {

    doInsertLogic();

} else if (Trigger.isUpdate) {

    doUpdateLogic();

}


10. Test for Recursion (Trigger.isExecuting + Static Variables)

Prevent triggers from calling themselves repeatedly by using a static flag.

public class PreventRecursion {

    public static Boolean firstRun = true;

}


11. Use @future or Queueable Wisely

Use asynchronous methods like @future or Queueable to handle long-running or post-commit actions.

@future

public static void sendWelcomeEmail(Id contactId) {

    // send email

}


12. Write Scalable Unit Tests for All Scenarios

Cover all logic paths with test data. Use System.assert() and test both positive and negative cases.

@isTest

private class AccountTriggerTest {

    static testMethod void testAccountInsert() {

        Account acc = new Account(Name=’Test’);

        insert acc;

        System.assertNotEquals(null, acc.Id);

    }

}

These practices ensure that your Apex triggers are not just functional, but also scalable, testable, and ready for enterprise-grade applications.


Common Apex Trigger Pitfalls (And How to Avoid Them)

Even if you’re following best practices, it’s easy to fall into traps that can quietly wreak havoc in your org. Let’s break down the most common Apex Trigger pitfalls and, more importantly, how to avoid them.


Using Multiple Triggers on the Same Object

Problem: Having multiple triggers on the same object is a common and dangerous mistake. Salesforce does not guarantee the order in which they execute.

Issues:

  • One trigger might depend on the result of another.
  • Unpredictable behavior during data updates.
  • Debugging becomes difficult.

✅ How to Avoid It: Stick to a “one trigger per object” rule and route all logic to a centralized handler class. This ensures predictability and easier maintenance.


Lack of Test Coverage

Problem: Without sufficient test coverage, you risk:

  • Failed deployments
  • Missed bugs
  • Unhandled edge cases

Many devs rush through unit testing just to meet the 75% requirement—but quality matters more than quantity.

✅ How to Avoid It:

  • Write tests for each trigger scenario (insert, update, delete).
  • Include System.assert statements—not just DML operations.
  • Cover both positive and negative paths.

@isTest

private class MyObjectTriggerTest {

    static testMethod void testInsert() {

        // Arrange

        MyObject__c obj = new MyObject__c(Name = ‘Test’);

        insert obj;

        // Assert

        System.assertNotEquals(null, obj.Id);

    }

}


Ignoring Bulk Operations

Problem: Testing with a single record doesn’t reflect real-world use. Salesforce frequently processes hundreds of records at once. If your trigger isn’t bulk-safe, it will fail.

✅ How to Avoid It:

  • Always use collections (List, Set, Map).
  • Write your logic to handle many records.
  • Test with at least 200 records in unit tests.

Uncontrolled Recursion

Problem: An endlessly looping trigger can be triggered when one action causes another insert/update, which in turn calls the same trigger.

✅ How to Avoid It: Use a static Boolean flag to ensure your trigger logic only runs once.

public class RecursionControl {

    public static Boolean isFirstRun = true;

}

Check it in your trigger:

if (RecursionControl.isFirstRun) {

    RecursionControl.isFirstRun = false;

    // safe logic here

}


Not Considering Future Limits

Problem: Developers often move logic to @future or Queueable and assume it will solve governor limit issues. However, async calls have their own limits.

✅ How to Avoid It:

  • Only use async logic when necessary.
  • Use Queueable over @future when possible.
  • Use batch jobs for processing large datasets.
  • Chain async logic responsibly to avoid exceeding limits.

Real-World Use Case: Scaling Apex Triggers in a 100K Record Data Load

Problem Statement

A Salesforce org needed to process and enrich 100,000 new records during a large data migration. The original trigger was written for small data volumes and began throwing governor limit errors.


Strategy Used

The dev team decided to:

  • Move all logic to a trigger handler class
  • Bulkify all SOQL and DML operations
  • Use Set<Id> and Map<Id, Object> to reduce redundant queries
  • Implement batching using Database.executeBatch to process records in chunks

Governor Limits Hit and Fixes Applied

Issues Encountered:

  • Too many SOQL queries (limit: 100)
  • Too many DML rows (limit: 10,000)

Fixes:

  • Combined SOQL into a single query outside of loops
  • Accumulated records into a list and did one update after processing
  • Used batching to keep operations within limits

Performance Comparison: Before vs. After

ScenarioBefore OptimizationAfter Optimization
Record LimitFailed at 10,000Handled 100,000 in 2,000-record batches
Processing Time30 minutesUnder 10 minutes
ErrorsGovernor limits hitNo errors

The outcome demonstrates how applying Apex trigger best practices can drastically improve performance, reduce errors, and make your automation scalable.


Apex Trigger Framework: Should You Use One?

Absolutely—especially if you’re building for a team, enterprise, or managed package. Frameworks help structure your code and keep it consistent across large orgs.


Benefits of a Trigger Framework

  • Keeps code modular and organized
  • Encourages separation of concerns (trigger logic vs. business logic)
  • Easier to test, maintain, and debug
  • Scales better in large Salesforce implementations

Top Framework Patterns

TDTM (Table-Driven Trigger Management)

Used in the Nonprofit Success Pack (NPSP), this model stores trigger logic in Custom Metadata and offers high configurability.

SFDX Plug-and-Play Models

Open-source solutions with lifecycle hooks like beforeInsert, afterUpdate, etc., providing clean structure for enterprise-scale triggers.

These patterns abstract away messy trigger logic and give you a consistent place to plug in your business rules.


How to Build Your Own Modular Framework

You don’t need to reinvent the wheel. A simple modular framework includes:

  • Trigger: One per object, delegates work to a handler
  • Handler Class: Manages logic per trigger event (insert/update/delete)
  • Helper Classes: Reusable utility functions

Example Setup:

trigger ContactTrigger on Contact (before insert, after update) {

    ContactTriggerHandler.handle(Trigger.isBefore, Trigger.isAfter, Trigger.new, Trigger.oldMap);

}

Your ContactTriggerHandler class would have methods like runBeforeInsert() and runAfterUpdate()—keeping your logic tidy, testable, and efficient.


Tools and Resources to Improve Your Trigger Development

Enhance your development experience and trigger quality by using the right tools, libraries, and monitoring systems.


Recommended GitHub Repos


VS Code Extensions for Apex Dev

  • Salesforce Extension Pack: The official toolkit for developing with Salesforce and Apex.
  • Apex PMD: A static code analysis tool that detects common bugs and enforces code quality.
  • Prettier Apex Plugin: Automatically formats your code for readability and consistency.

Salesforce Logging Tools & Performance Monitors

  • Debug Logs (Setup > Debug Logs): Track trigger execution and inspect runtime data.
  • Developer Console: Analyze SOQL queries, DML operations, and trigger performance.
  • Salesforce CLI: Automate workflows, query data, and monitor executions via terminal commands.

These tools help enforce best practices, catch issues early, and keep your automation scalable and maintainable.


Final Thoughts – How to Master Apex Triggers as a Developer

When to Refactor Old Triggers

If you inherit legacy triggers that aren’t bulkified, testable, or scalable—refactor them using a handler pattern. Start by moving logic to a class, then introduce unit tests and modern practices.

Embracing Declarative Alternatives

Don’t use a trigger just because you can. If a Flow or Validation Rule solves the problem, go that route. Apex should be reserved for logic that can’t be achieved declaratively.

Staying Updated with Salesforce Releases

Salesforce adds new capabilities every release. Stay on top of changes by:

Being a great trigger developer means more than writing code—it’s about knowing when, why, and how to use Apex responsibly.


FAQs About Apex Triggers (For SEO and Rich Snippets)

Can I have more than one trigger on the same object?
Technically yes, but it’s not recommended. Salesforce doesn’t guarantee execution order, which can cause unexpected results. Stick to one trigger per object and route all logic to a handler class.

What are the best tools for debugging triggers?

  • Debug Logs in Setup
  • Developer Console for execution context
  • Salesforce CLI for running queries and deploying changes
  • PMD for static code analysis

Should I always bulkify my trigger logic?
Yes! Always assume that your trigger could handle many records at once. Bulkification helps you avoid hitting governor limits and ensures that your trigger scales with your data volume.

What is the difference between before and after triggers?

  • Before Triggers run before Salesforce saves the record—great for validations and field updates.
  • After Triggers run after the record is saved—ideal for actions involving related records or external systems.

Can I call a Queueable class from a trigger?
Yes, Queueables are preferred over @future methods for most async operations. Just make sure your logic follows async best practices and governor limits.

How do I stop triggers from firing recursively?
Use a static Boolean flag in a helper class to prevent recursion. This ensures your logic runs only once per transaction.

Are Flows better than Apex Triggers?
It depends. Flows are easier to maintain for simpler logic. But if the business rules are complex or involve multiple objects, triggers offer more flexibility and control.

What is the best practice for handling errors in triggers?
Wrap DML operations in try-catch blocks, log exceptions using a custom logging utility, and notify admins if critical failures occur. This helps improve transparency and debugging.

How do I handle triggers during large data loads or integrations?
Use batch classes for large volumes, minimize trigger logic complexity, and implement flags or bypass conditions for trusted integration users or use cases.


Download: Apex Trigger Best Practice Template (Free)

Want to build your own Apex Trigger like a pro? Download this free package:

✅ .cls Trigger Handler Template
✅ Lifecycle Flowchart Diagram
✅ Code Checklist for Reviews

Download Template Here

0
Based on 0 ratings
9445ab50a90abd0bd1934e3ae4e12c27
Ansarbasha B
+ posts