
Table of Contents
Introduction to Apex Triggers
Alright, let’s talk Apex Triggers. If you’ve spent some time in the Salesforce ecosystem, you’ve probably heard this term being tossed around a lot.
What Are Apex Triggers?
Apex Triggers are simply pieces of code that execute before or after a specific database event occurs. The following are the events that activate Apex Triggers :
- insert: A new record is created.
- update: An existing record is changed.
- delete: A record is deleted.
- undelete: A record is recovered from the Recycle Bin.
Salesforce Apex triggers wait for one of these events to happen to an object, like an Account, Contact, or a custom object you’ve built. The moment it detects an event, it runs the code you’ve written. While Flows and Process Builders are amazing for declarative automation, they have their limits. When the logic becomes too complex, that is, when you need to perform DML operations on multiple objects or integrate with external systems, that’s when you need to use Apex Triggers, while following Apex Triggers Best Practices. They let you cook up some seriously wild business logic—stuff you just can’t pull off with those drag-and-drop tools, no matter how many times you click around.
When to Use Triggers in Salesforce
- Wild Validation Rules: Say you’ve got a custom object, and you need to check if a field’s legit based on some formula that grabs data from a related record—yeah, a trigger’s your go-to. If your business rules are a tangled mess, Apex triggers can slam the door on records that don’t play by your rules.
- Cross-Object Domino Effect: Picture this. Someone updates the “Status” on a parent Opportunity, and suddenly, you need to make sure all its child OpportunityLineItems get the memo too. That’s trigger territory—perfect for those chain-reaction updates.
- Blocking Deletions: Maybe you’re tired of users nuking Contact records that still have open Cases. With a trigger, you can swoop in, spot those lingering Cases, and hit ‘em with an error—no deletions for you, buddy.
- Plugging Into Other Systems: Sure, most of the time you’d let Apex classes handle external API stuff, but sometimes you want a trigger to light the fuse. When a certain record gets created or changed, the trigger fires, and an API callout happens.
Types of Apex Triggers
The following are two fundamental types of Apex triggers: Before Triggers and After Triggers. Knowing the difference is key to writing effective, efficient, and bug-free code.
Before Triggers:
- A Before Trigger executes before the record is actually saved. This gives you an opportunity to inspect the record, change its data, and even stop it from getting in if it doesn’t meet the rules.
- The best part about a Before Trigger is its efficiency.
- You can modify the record’s fields directly without having to use a DML operation. This means no extra database calls, which improves performance.
After Triggers:
- An After Trigger executes after the record has been saved to the database.
- The record now has a brand-new ID and has taken its place among all the other records.
- The key thing to understand here is that because the record is already saved, you can’t use an After Trigger to modify the same record that initiated the trigger without performing another DML operation (an update statement).
- However, After Triggers are perfect for a different set of complex tasks.
- The biggest advantage of an After Trigger is that the record has a unique ID, and all its fields are finalized. This means you can use the ID to perform operations on other records.
The Dos: Apex Triggers Best Practices
Think of the following pointers as the rules you should follow while writing a trigger:
- One Trigger Per Object: You should only have one trigger for each object in your Salesforce org. Salesforce doesn’t guarantee the order of execution. This means your Salesforce Apex triggers could run in any random sequence, leading to unpredictable results. A single trigger on an object acts as a central control panel. It knows when and how to fire off the different pieces of logic.
- Use an Apex Triggers’ Handler or a Framework: If you have only one trigger per object, where does all the code go? You don’t just dump all your business logic into one massive trigger file. The solution is to use a trigger handler class. The trigger itself should be as simple and lightweight as possible. It should only be responsible for one thing: calling a method in a separate, dedicated Apex class-the handler. It grabs whatever’s happening (like Trigger.new or Trigger.old) and tosses it over to the handler, then sits back and lets the handler handle it.
- Avoid SOQL Queries and DML Statements Inside Loops: This is the direct consequence of bulkification. Never, ever perform SOQL queries or DML statements inside a for loop. Always stash whatever IDs or data you need into a list or, even better, a Set, and then do your database magic just once, outside the loop.
- Use Sets for Unique IDs: Here’s the deal: when you’re grabbing IDs for related records, a Set is your best buddy. Why? Sets only keep unique stuff—no duplicates sneaking in. So, if you’re looping through records and shoving IDs into a Set, you’re dodging that awkward “why did I query the same record twelve times?” conversation.
- Bulkify Your Code: Bulkification means writing your code to handle a collection of records—a list or a set—rather than just one record at a time. What if a data loader inserts 200 records at once? Your trigger will fire for all 200 records in a single execution context. If you write your code to make a SOQL query or a DML statement inside a for loop that iterates over each record, you will certainly hit a governor limit. Salesforce has a limit of 100 SOQL queries and 150 DML statements per transaction. A non-bulkified trigger would fail after just 100 records. A bulkified trigger collects all the necessary information from the entire list of records and then performs one single SOQL query or DML statement outside the loop.
For example, instead of this (bad):
Apex
for(Account a : Trigger.new){
Contact c = new Contact(AccountId = a.Id, LastName = 'Smith');
insert c;
}
Do this(Good):
Apex
List<Contact> contactsToInsert = new List<Contact>();
for(Account a : Trigger.new){
contactsToInsert.add(new Contact(AccountId = a.Id, LastName = 'Smith'));
}
insert contactsToInsert;
This might seem like a small change, but it’s the difference between a trigger that works fine for a few records and one that can handle thousands without breaking a sweat.
The Don’ts: Apex Trigger Fails
- Don’t Dump Business Logic Straight Into the Trigger: Please don’t turn your trigger into a complicated mess. Keep it tight—call out to your handler class and let that handle the heavy lifting. If you jam all your business logic in the trigger, good luck figuring out what’s going on when you’re fixing bugs at 2 AM.
- Don’t Use Future Methods or Callouts Without Thinking: Async processing is a cool feature to use, but it’s not free-for-all. You can’t just make HTTP callouts or toss stuff into a @future method straight from your trigger without planning. Always make sure you actually need async, and watch out for those recursive nightmares. Always have a clear strategy and use static variables or other mechanisms to prevent recursion when you’re dealing with asynchronous processes.
- Don’t Assume a Single Record Context: This goes back to bulkification. Always assume your trigger will be processing a list of records. Even if you’re only testing with a single record, your code needs to be ready for a batch job that processes hundreds or thousands of records at once.
Don’t Hardcode IDs: You just want to type in that 18-character ID and call it a day. But the second you move that code to another org, it will break. Instead, query for what you need based on something that doesn’t change, like a developer’s name or some unique field. For RecordTypes, just do:
RecordType rt = [SELECT Id FROM RecordType WHERE SobjectType = 'Account' AND DeveloperName = 'My_Record_Type_Name' LIMIT 1];
Now your code works everywhere, not just in your sandbox.
Common Mistakes and How to Fix Them
We all make mistakes when we’re learning how to use a new and powerful tool like Apex Triggers. The key isn’t to never make a mistake, but to know how to fix them before they cause a meltdown in your Salesforce org.
Mistake #1: The Non-Bulkified Trigger
This makes your code work beautifully for a single record but it completely falls apart when a data loader hits it with 200 at once. The symptom? A System.LimitException: Too many SOQL queries: 101 or Too many DML statements: 151.
The Fix: The solution is bulkification. Always assume your trigger is handling a list of records. The simplest way to do this is to move any database operations (SOQL queries, DML statements) outside of your for loops.
When you’re writing a for loop, pretend there’s a tiny, furious Salesforce governor standing right next to you, watching your every move. The moment you type [SELECT…] or insert inside that loop, he’s going to shout “Governor Limit!” in your ear. The way to make him happy is to collect all the data you need into a Set<Id> or List<sObject> and then perform the operation just once after the loop has finished its work.
Mistake #2: The Trigger Recursion Loop
A recursion loop happens when a trigger’s action causes the same trigger to fire again and again, until it hits the ‘Maximum trigger depth exceeded’ error.
A classic example:
- An After Update trigger on Account updates a field.
- The field update saves the record.
- The save operation triggers the After Update Apex triggers.
- The loop continues indefinitely.
It keeps going until the system finally gives up and throws an error.
The Fix: The most common and effective way to prevent recursion is by using a simple static variable as a gatekeeper.
Apex
public class AccountTriggerHandler {
public static Boolean hasRun = false;
public void afterUpdate(List<Account> newList, Map<Id, Account> oldMap)
{if (!hasRun)
{hasRun = true;
// Your logic here that might update the same account
// e.g., update related contacts
}
}
}
In this pattern, the hasRun variable is set to true on the first execution, and it ensures that the core logic of the trigger handler will only ever execute once per transaction. This prevents your trigger from getting stuck in an endless loop.
Mistake #3: Hardcoding IDs and Other Values
IDs for records, record types, and other metadata are unique to each Salesforce environment. What’s 01230000000ABC in your Dev Org will be something entirely different in Production. Using a hardcoded ID is like giving someone a street address in one city and expecting them to find it in a completely different city. It just won’t work.
The Fix: Never hardcode IDs. Instead, use SOQL queries to dynamically retrieve the IDs you need based on a stable, unique identifier like a Name or a DeveloperName.
Instead of this (bad):
Account a = new Account(RecordTypeId = '01230000000ABC', Name = 'Test Account');
Do this (good):
Id recordTypeId = Schema.SObjectType.Account.getRecordTypeInfosByName().get('Business').getRecordTypeId(); Account a = new Account(RecordTypeId = recordTypeId, Name = 'Test Account');
This approach ensures that your code is portable and will work in all environments.
Mistake #4: Writing a Single-Trigger-Event Trigger
Sometimes you’ll see a developer write a trigger like trigger AccountTrigger on Account (after insert) { … }. This violates the “one trigger per object” best practice. What happens six months from now when a new developer needs to add some logic for an after update? They’ll create AccountTriggerUpdate and you’ll be back to square one with an unmanageable mess.
The Fix: Write a single trigger for all events on an object, right from the start.
Apex
trigger AccountTrigger on Account (before insert, before update, after insert, after update)
{
// This is the only trigger on the Account object.
}
The handler class you’ve built will then contain separate methods for each event, and your trigger simply decides which method to call based on the context variables (Trigger.isInsert, Trigger.isUpdate, etc.). It’s clean, it’s organized, and it sets the stage for a sustainable codebase.
Trigger Frameworks
A lot of these best practices—the single trigger per object, the handler class, the recursion guard—all point toward a more structured way of writing code. This structured approach is what we call a Trigger Framework.
Popular Trigger Frameworks in the Salesforce Community
There are several popular frameworks that the Salesforce community uses. They range from handler-based patterns to more complex, enterprise-level solutions.
- The Simple Handler Pattern: A trigger calls a single static method on a handler class, and that class contains all the logic.
- Kevin O’Hara’s Trigger Framework: This is a more advanced framework that uses an abstract class and interfaces to create a highly reusable and testable structure. It’s meant for developers working on more complex orgs.
- The FFLib Apex Common Library: This is an open-source library that goes beyond just Apex triggers. It implements a full application architecture (the Domain, Selector, Service, and Repository pattern) to create a robust and highly scalable codebase. While it has a steeper learning curve, it’s considered a gold standard for enterprise-level Salesforce development.
Testing Triggers the Right Way
In accordance with Apex Triggers Best Practices, every piece of Apex code you write, including your Salesforce Appex triggers, must be accompanied by a test class. This is a requirement for deployment to production (you need at least 75% code coverage). A good test class proves that your trigger works as intended and doesn’t break anything else.
- What does a legit test class look like? First off, you must build your own data. Don’t hope there’s already some random record in your sandbox. Your test should be a complete little universe—it sets up everything it needs and doesn’t depend on anything outside itself. Use Test.startTest() and Test.stopTest(). These reset your governor limits, so you’ve got a clean slate to work with. It’s like hitting the refresh button for your test run.
- Don’t lean on existing data. Seriously, that’s how you end up with flaky tests that work one day and bomb the next time they are deployed. Relying on sandbox records is like building a house on sand—zero foundation. The right way? Spin up all your data inside the test class.
- Testing for Different Scenarios: You need to test for:
- Single Record: What happens when just one record is created or updated?
- Bulk Records: Can your Apex triggers handle a list of 200 records at once? This is where bulkification is put to the test.
- Negative Scenarios: Do your validation Apex triggers correctly throw an error when a user tries to save a record with invalid data?
- Edge Cases: A good test class includes test methods for all of these scenarios.
- Using System.assert Statements: The purpose of a test is to verify an outcome. You do this with System.assert statements. If your Apex triggers are supposed to create a related contact, your test should query for that contact and assert that it exists and has the correct field values.
- System.assertEquals(expectedValue, actualValue, ‘Your helpful error message’);
- System.assert(condition, ‘Another helpful error message’);
These statements are proofs that your trigger is doing its job.
- Golden Rule: Test the Handler, Not Just the Apex Triggers: Call those handler methods directly in your unit tests. Like, let’s say you’ve got a method called updateRelatedContacts in your handler. Spin up a test method, toss it a batch of fake accounts, and then double-check that your contacts actually got updated. That’s how you know your logic’s solid, isolated, and not just accidentally working.
- The Developer Console: The Developer Console is where you peek under the hood when the Salesforce Apex triggers go sideways.
- Execution Logs: Every time you run something, Salesforce spits out a log with all the nitty-gritty governor limits. Switch on “Finest” logging for Apex if you want to see every little detail.
- Debug Statements: Sprinkle System.debug() statements everywhere: They are your virtual breadcrumbs. Log the values of variables, the IDs of records, and the paths your code is taking. A good practice is to always wrap your debug statements in if(LoggingLevel.isFinest()) so you can easily turn them off in production.
- Using try-catch Blocks: Wrapping your Apex triggers logic in try-catch blocks is a smart way to handle unexpected errors. Instead of a transaction failing outright, you can catch the exception, log it, and potentially even send an email notification to an administrator. This gives you a chance to investigate the issue without causing a complete user-facing failure.
- Apex PMD: The Code Analyzer: Apex PMD is a static code analysis tool that can be integrated into your development environment. It scans your code for common mistakes like non-bulkified queries, inefficient loops, and other violations of best practices.

Conclusion
We’ve talked about the common mistakes to avoid, the power of trigger frameworks, and why rigorous testing is not just a chore but a necessity. By following these Apex Triggers Best Practices, you can write Salesforce Apex triggers that are not just effective, but also reliable, scalable, and a pleasure to work with. Now go forth and build something amazing—and don’t forget to write your test class!
Mastering Apex Triggers best practices is essential for any Salesforce professional looking to build scalable, maintainable, and error-free applications in 2025. From writing bulk-safe triggers and following the one-trigger-per-object rule to avoiding hard-coded IDs and ensuring proper test coverage, these dos and don’ts can well be the difference between a smooth-running Salesforce org and a maintenance nightmare.
By committing to clean, optimized, and future-proof code, you not only improve system performance but also enhance your career prospects in the Salesforce ecosystem. If you’re serious about becoming an expert, it’s time to learn from the best. AlmaMate Info Tech, the best Salesforce training company offering the best Salesforce training in Noida, provides hands-on, industry-focused programs designed to help you master Apex, declarative development, and real-world Salesforce applications. Enroll today and take the next step toward becoming a top-tier Salesforce professional.