If you’re starting your journey as a Salesforce developer, understanding Apex triggers is crucial to building powerful, automated solutions on the Salesforce platform. This comprehensive Salesforce Apex triggers tutorial will walk you through everything you need to know—from basic concepts to practical, real-world examples.

Whether you’re searching for apex trigger examples or want to master apex triggers beginner concepts, this guide has you covered.
Let’s dive in!
What are Apex Triggers?
Apex triggers are pieces of code that execute automatically before or after specific data manipulation language (DML) events occur in Salesforce. Think of triggers as “event handlers” that respond to database operations like inserting, updating, deleting, or undeleting records.
In simpler terms, whenever someone creates, updates, or deletes a record in Salesforce, you can write code that runs automatically in response to that action—without any user intervention.
Key Characteristics of Apex Triggers:
- Automatic execution: No manual intervention required
- Event-driven: Responds to database operations
- Object-specific: Written for specific Salesforce objects (Account, Contact, Custom Objects, etc.)
- Powerful automation: Enables complex business logic beyond what workflows or Process Builder can handle

Why Use Apex Triggers?
Before we jump into our apex triggers beginner tutorial, let’s understand why triggers are essential:
1. Complex Business Logic
When declarative tools (Workflow Rules, Process Builder, Flow) can’t meet your requirements, triggers provide unlimited flexibility.
2. Data Validation
Enforce custom validation rules that go beyond standard validation rules.
3. Cross-Object Updates
Update related records across different objects automatically.
4. Integration
Call external web services when records are created or modified.
5. Audit Trails
Create custom logging and tracking mechanisms.
Real-World Scenarios:
- Automatically creating related records when an Opportunity is closed
- Preventing deletion of Accounts with associated Opportunities
- Calculating and updating commission amounts for sales reps
- Sending data to external systems when records change
Understanding Trigger Structure
Every Apex trigger follows a standard structure. Let’s break it down step by step:
Basic Trigger Syntax
apextrigger TriggerName on ObjectName (trigger_events) {
// Your code here
}
Complete Trigger Structure Example
apextrigger AccountTrigger on Account (before insert, before update, after insert, after update) {
// Before Insert Logic
if (Trigger.isBefore && Trigger.isInsert) {
// Code executed before records are inserted
}
// Before Update Logic
if (Trigger.isBefore && Trigger.isUpdate) {
// Code executed before records are updated
}
// After Insert Logic
if (Trigger.isAfter && Trigger.isInsert) {
// Code executed after records are inserted
}
// After Update Logic
if (Trigger.isAfter && Trigger.isUpdate) {
// Code executed after records are updated
}
}
Breaking Down the Components:
- trigger: Keyword to declare a trigger
- TriggerName: Descriptive name for your trigger
- on ObjectName: Specifies which Salesforce object this trigger applies to
- trigger_events: When the trigger should fire (before/after insert, update, delete, undelete)
Trigger Context Variables Explained
Trigger context variables are special variables available in every trigger that provide information about the runtime context. Understanding these is critical for any Salesforce Apex triggers tutorial.
Essential Context Variables
| Context Variable | Description | Available In |
|---|---|---|
Trigger.new | List of new versions of records | insert, update, undelete |
Trigger.old | List of old versions of records | update, delete |
Trigger.newMap | Map of IDs to new versions of records | before update, after insert, after update, after undelete |
Trigger.oldMap | Map of IDs to old versions of records | update, delete |
Trigger.isInsert | Returns true if fired due to insert | All insert operations |
Trigger.isUpdate | Returns true if fired due to update | All update operations |
Trigger.isDelete | Returns true if fired due to delete | All delete operations |
Trigger.isUndelete | Returns true if fired due to undelete | All undelete operations |
Trigger.isBefore | Returns true if before trigger | All before triggers |
Trigger.isAfter | Returns true if after trigger | All after triggers |
Trigger.size | Total number of records in trigger | All triggers |
Practical Example Using Context Variables
apextrigger ContactTrigger on Contact (before insert, before update) {
for (Contact con : Trigger.new) {
// Check if this is an insert operation
if (Trigger.isInsert) {
System.debug('New contact being created: ' + con.Name);
}
// Check if this is an update operation
if (Trigger.isUpdate) {
// Get the old version of the record
Contact oldContact = Trigger.oldMap.get(con.Id);
// Check if email changed
if (con.Email != oldContact.Email) {
System.debug('Email changed from ' + oldContact.Email + ' to ' + con.Email);
}
}
}
}
Types of Apex Triggers
Apex triggers are categorized based on when they execute relative to the database operation.
1. Before Triggers
Execute before the record is saved to the database.
Use Cases:
- Validating record data
- Updating fields in the same record
- Preparing data before it’s committed
Example:
apextrigger LeadBeforeTrigger on Lead (before insert, before update) {
for (Lead ld : Trigger.new) {
// Automatically set Lead Source if not provided
if (String.isBlank(ld.LeadSource)) {
ld.LeadSource = 'Web';
}
}
}
2. After Triggers
Execute after the record is saved to the database.
Use Cases:
- Creating related records
- Updating other objects
- Sending emails or notifications
- Calling external systems
Example:
apextrigger OpportunityAfterTrigger on Opportunity (after insert) {
List<Task> tasksToInsert = new List<Task>();
for (Opportunity opp : Trigger.new) {
if (opp.Amount > 100000) {
Task t = new Task();
t.Subject = 'Follow up on high-value opportunity';
t.WhatId = opp.Id;
t.ActivityDate = Date.today().addDays(7);
tasksToInsert.add(t);
}
}
if (!tasksToInsert.isEmpty()) {
insert tasksToInsert;
}
}
Real-Time Apex Trigger Examples
Now let’s explore practical apex trigger examples that you can use in real-world scenarios.
Example 1: Prevent Duplicate Accounts by Email Domain
Business Requirement: Prevent creating accounts with the same email domain.
apextrigger AccountDuplicatePrevention on Account (before insert, before update) {
// Collect email domains from new/updated accounts
Set<String> emailDomains = new Set<String>();
for (Account acc : Trigger.new) {
if (String.isNotBlank(acc.Website)) {
String domain = acc.Website.toLowerCase();
emailDomains.add(domain);
}
}
// Query existing accounts with same domains
List<Account> existingAccounts = [
SELECT Id, Website
FROM Account
WHERE Website IN :emailDomains
AND Id NOT IN :Trigger.newMap.keySet()
];
// Create a set of existing domains
Set<String> existingDomains = new Set<String>();
for (Account acc : existingAccounts) {
existingDomains.add(acc.Website.toLowerCase());
}
// Add error to duplicate accounts
for (Account acc : Trigger.new) {
if (String.isNotBlank(acc.Website)) {
String domain = acc.Website.toLowerCase();
if (existingDomains.contains(domain)) {
acc.Website.addError('An account with this website already exists.');
}
}
}
}
Key Learning Points:
- Using
Setto collect unique values - SOQL query to check existing records
addError()method to prevent record saving- Excluding current records using
Trigger.newMap.keySet()
Example 2: Auto-Create Contact When Account is Created
Business Requirement: Automatically create a primary contact whenever a new account is created.
apextrigger AccountContactCreation on Account (after insert) {
List<Contact> contactsToInsert = new List<Contact>();
for (Account acc : Trigger.new) {
Contact con = new Contact();
con.AccountId = acc.Id;
con.LastName = acc.Name + ' - Primary Contact';
con.Email = 'info@' + acc.Name.replace(' ', '').toLowerCase() + '.com';
con.Phone = acc.Phone;
contactsToInsert.add(con);
}
if (!contactsToInsert.isEmpty()) {
insert contactsToInsert;
}
}
Key Learning Points:
- Using
after insertto access record IDs - Bulk insert pattern with List collection
- Checking list is not empty before DML operation
Example 3: Update Parent Account When Opportunity Stage Changes
Business Requirement: Update Account’s “Last Opportunity Stage” field when any related Opportunity stage changes.
apextrigger OpportunityStageUpdate on Opportunity (after update) {
// Collect Account IDs
Set<Id> accountIds = new Set<Id>();
for (Opportunity opp : Trigger.new) {
Opportunity oldOpp = Trigger.oldMap.get(opp.Id);
// Check if stage changed
if (opp.StageName != oldOpp.StageName && opp.AccountId != null) {
accountIds.add(opp.AccountId);
}
}
// Query and update accounts
if (!accountIds.isEmpty()) {
List<Account> accountsToUpdate = new List<Account>();
for (Id accId : accountIds) {
Account acc = new Account();
acc.Id = accId;
acc.Last_Opportunity_Stage__c = 'Updated on ' + System.now();
accountsToUpdate.add(acc);
}
update accountsToUpdate;
}
}
Key Learning Points:
- Comparing old and new values using
Trigger.oldMap - Collecting parent record IDs
- Updating parent records efficiently
- Null checks for lookup relationships
Example 4: Prevent Deletion of Accounts with Opportunities
Business Requirement: Don’t allow users to delete accounts that have associated opportunities.
apextrigger AccountDeletionPrevention on Account (before delete) {
// Collect Account IDs being deleted
Set<Id> accountIds = Trigger.oldMap.keySet();
// Query for accounts with opportunities
Map<Id, Account> accountsWithOpps = new Map<Id, Account>([
SELECT Id, (SELECT Id FROM Opportunities LIMIT 1)
FROM Account
WHERE Id IN :accountIds
]);
// Check each account being deleted
for (Account acc : Trigger.old) {
Account accWithOpps = accountsWithOpps.get(acc.Id);
if (accWithOpps != null && accWithOpps.Opportunities.size() > 0) {
acc.addError('Cannot delete account with associated opportunities.');
}
}
}
Key Learning Points:
- Using
before deletetrigger - Accessing records via
Trigger.old - Subquery to check related records
- Preventing deletion with
addError()
Example 5: Update Total Revenue on Account
Business Requirement: Calculate and update total revenue on Account based on all closed-won Opportunities.
apextrigger OpportunityRevenueCalculation on Opportunity (after insert, after update, after delete) {
Set<Id> accountIds = new Set<Id>();
// Collect Account IDs from new/updated opportunities
if (Trigger.isInsert || Trigger.isUpdate) {
for (Opportunity opp : Trigger.new) {
if (opp.AccountId != null) {
accountIds.add(opp.AccountId);
}
}
}
// Collect Account IDs from deleted opportunities
if (Trigger.isDelete) {
for (Opportunity opp : Trigger.old) {
if (opp.AccountId != null) {
accountIds.add(opp.AccountId);
}
}
}
if (!accountIds.isEmpty()) {
// Aggregate total revenue for each account
Map<Id, Decimal> accountRevenueMap = new Map<Id, Decimal>();
for (AggregateResult ar : [
SELECT AccountId, SUM(Amount) totalRevenue
FROM Opportunity
WHERE AccountId IN :accountIds
AND StageName = 'Closed Won'
GROUP BY AccountId
]) {
accountRevenueMap.put(
(Id)ar.get('AccountId'),
(Decimal)ar.get('totalRevenue')
);
}
// Update accounts
List<Account> accountsToUpdate = new List<Account>();
for (Id accId : accountIds) {
Account acc = new Account();
acc.Id = accId;
acc.Total_Revenue__c = accountRevenueMap.containsKey(accId)
? accountRevenueMap.get(accId)
: 0;
accountsToUpdate.add(acc);
}
update accountsToUpdate;
}
}
Key Learning Points:
- Handling multiple trigger events (insert, update, delete)
- Using AggregateResult for calculations
- Map for efficient data lookup
- Ternary operator for conditional assignment
Example 6: Populate Field Based on Another Field Value
Business Requirement: Automatically set Account Rating based on Annual Revenue.
apextrigger AccountRatingAssignment on Account (before insert, before update) {
for (Account acc : Trigger.new) {
if (acc.AnnualRevenue != null) {
if (acc.AnnualRevenue >= 10000000) {
acc.Rating = 'Hot';
} else if (acc.AnnualRevenue >= 5000000) {
acc.Rating = 'Warm';
} else {
acc.Rating = 'Cold';
}
}
}
}
Key Learning Points:
- Using
beforetrigger to modify the same record - Conditional logic with if-else statements
- Direct field assignment without DML
Best Practices for Apex Triggers
Following best practices is crucial for writing maintainable, efficient triggers. Here are the golden rules every Salesforce developer should follow:
1. One Trigger Per Object
Why? Multiple triggers on the same object execute in unpredictable order.
Best Practice: Create one trigger per object and use handler classes.
apex// Good Practice - Single Trigger
trigger AccountTrigger on Account (before insert, before update, after insert, after update) {
AccountTriggerHandler.handleTrigger();
}
apex// Handler Class
public class AccountTriggerHandler {
public static void handleTrigger() {
if (Trigger.isBefore) {
if (Trigger.isInsert) {
handleBeforeInsert(Trigger.new);
} else if (Trigger.isUpdate) {
handleBeforeUpdate(Trigger.new, Trigger.oldMap);
}
}
if (Trigger.isAfter) {
if (Trigger.isInsert) {
handleAfterInsert(Trigger.new);
} else if (Trigger.isUpdate) {
handleAfterUpdate(Trigger.new, Trigger.oldMap);
}
}
}
private static void handleBeforeInsert(List<Account> newAccounts) {
// Before insert logic
}
private static void handleBeforeUpdate(List<Account> newAccounts, Map<Id, Account> oldMap) {
// Before update logic
}
private static void handleAfterInsert(List<Account> newAccounts) {
// After insert logic
}
private static void handleAfterUpdate(List<Account> newAccounts, Map<Id, Account> oldMap) {
// After update logic
}
}
2. Bulkify Your Code
Why? Triggers process records in batches (up to 200). Non-bulkified code hits governor limits.
Bad Practice:
apex// DON'T DO THIS - SOQL in loop
trigger BadPractice on Contact (after insert) {
for (Contact con : Trigger.new) {
Account acc = [SELECT Id, Name FROM Account WHERE Id = :con.AccountId];
// Process account
}
}
Good Practice:
apex// DO THIS - SOQL outside loop
trigger GoodPractice on Contact (after insert) {
Set<Id> accountIds = new Set<Id>();
for (Contact con : Trigger.new) {
accountIds.add(con.AccountId);
}
Map<Id, Account> accountMap = new Map<Id, Account>([
SELECT Id, Name
FROM Account
WHERE Id IN :accountIds
]);
for (Contact con : Trigger.new) {
Account acc = accountMap.get(con.AccountId);
// Process account
}
}
3. Avoid Recursive Triggers
Why? Triggers can call themselves indefinitely, causing errors.
Solution: Use static variables to prevent recursion.
apex// Helper Class
public class TriggerHelper {
public static Boolean isAccountTriggerExecuted = false;
}
apex// Trigger with recursion prevention
trigger AccountTrigger on Account (after update) {
if (!TriggerHelper.isAccountTriggerExecuted) {
TriggerHelper.isAccountTriggerExecuted = true;
// Your logic here
TriggerHelper.isAccountTriggerExecuted = false;
}
}
4. Use Collections (Set, List, Map)
Collections help process records efficiently and avoid governor limits.
apextrigger ContactTrigger on Contact (after insert) {
// Use Set for unique values
Set<Id> accountIds = new Set<Id>();
// Use List for ordered collections
List<Task> tasksToInsert = new List<Task>();
// Use Map for key-value lookups
Map<Id, Account> accountMap = new Map<Id, Account>();
}
5. Handle Exceptions Gracefully
apextrigger OpportunityTrigger on Opportunity (before insert) {
try {
// Your logic
} catch (DmlException e) {
System.debug('DML Exception: ' + e.getMessage());
// Log error or send notification
} catch (Exception e) {
System.debug('General Exception: ' + e.getMessage());
}
}
6. Use Before Triggers for Same-Record Updates
Why? Before triggers don’t require additional DML operations for the same record.
apex// Efficient - Before trigger
trigger AccountTrigger on Account (before insert) {
for (Account acc : Trigger.new) {
acc.Rating = 'Hot'; // No DML needed
}
}
7. Write Test Classes
Always write test coverage (minimum 75% for deployment).
apex@isTest
public class AccountTriggerTest {
@isTest
static void testAccountCreation() {
// Create test data
Account acc = new Account();
acc.Name = 'Test Account';
acc.AnnualRevenue = 12000000;
Test.startTest();
insert acc;
Test.stopTest();
// Verify results
Account insertedAccount = [SELECT Id, Rating FROM Account WHERE Id = :acc.Id];
System.assertEquals('Hot', insertedAccount.Rating, 'Rating should be Hot');
}
}
Advanced Trigger Examples
Let’s explore more sophisticated apex trigger examples that handle complex business scenarios.
Example 7: Cascade Updates to Child Records
Business Requirement: When an Account’s industry changes, update all related Contacts with a timestamp.
apextrigger AccountIndustryChange on Account (after update) {
// Collect Account IDs where industry changed
Set<Id> accountIdsWithIndustryChange = new Set<Id>();
for (Account acc : Trigger.new) {
Account oldAcc = Trigger.oldMap.get(acc.Id);
// Check if industry field changed
if (acc.Industry != oldAcc.Industry) {
accountIdsWithIndustryChange.add(acc.Id);
}
}
if (!accountIdsWithIndustryChange.isEmpty()) {
// Query all contacts related to these accounts
List<Contact> contactsToUpdate = [
SELECT Id, Account_Industry_Updated__c
FROM Contact
WHERE AccountId IN :accountIdsWithIndustryChange
];
// Update the timestamp field
for (Contact con : contactsToUpdate) {
con.Account_Industry_Updated__c = System.now();
}
// Bulk update
if (!contactsToUpdate.isEmpty()) {
update contactsToUpdate;
}
}
}
Key Learning Points:
- Detecting specific field changes
- Cascading updates to child records
- Using System.now() for timestamps
- Efficient bulk processing
Example 8: Maintain Data Consistency Across Objects
Business Requirement: When a Contact’s email is updated, check for duplicates across all Contacts and flag them.
apextrigger ContactEmailDuplicateCheck on Contact (before insert, before update) {
// Collect all email addresses
Set<String> emailAddresses = new Set<String>();
for (Contact con : Trigger.new) {
if (String.isNotBlank(con.Email)) {
emailAddresses.add(con.Email.toLowerCase());
}
}
if (!emailAddresses.isEmpty()) {
// Query existing contacts with same emails (excluding current records)
Map<String, List<Contact>> emailToContactsMap = new Map<String, List<Contact>>();
List<Contact> existingContacts = [
SELECT Id, Email, FirstName, LastName, AccountId
FROM Contact
WHERE Email IN :emailAddresses
AND Id NOT IN :Trigger.newMap.keySet()
];
// Build map of email to contacts
for (Contact existing : existingContacts) {
String emailKey = existing.Email.toLowerCase();
if (!emailToContactsMap.containsKey(emailKey)) {
emailToContactsMap.put(emailKey, new List<Contact>());
}
emailToContactsMap.get(emailKey).add(existing);
}
// Check each new/updated contact
for (Contact con : Trigger.new) {
if (String.isNotBlank(con.Email)) {
String emailKey = con.Email.toLowerCase();
if (emailToContactsMap.containsKey(emailKey)) {
List<Contact> duplicates = emailToContactsMap.get(emailKey);
// Add warning (not error, just flag)
con.Potential_Duplicate__c = true;
con.Duplicate_Count__c = duplicates.size();
// Optionally, add error to prevent save
// con.Email.addError('Email already exists: ' + duplicates[0].FirstName + ' ' + duplicates[0].LastName);
}
}
}
}
}
Key Learning Points:
- Advanced duplicate detection
- Using Map with List values
- Conditional error vs warning
- Case-insensitive string comparison
Example 9: Automatic Task Creation Based on Stage
Business Requirement: Create specific tasks when an Opportunity reaches certain stages.
apextrigger OpportunityTaskAutomation on Opportunity (after insert, after update) {
List<Task> tasksToCreate = new List<Task>();
// Define stage-to-task mapping
Map<String, String> stageTaskMap = new Map<String, String>{
'Proposal/Price Quote' => 'Send proposal and follow up within 48 hours',
'Negotiation/Review' => 'Schedule negotiation meeting with decision makers',
'Closed Won' => 'Send welcome package and schedule kickoff call'
};
for (Opportunity opp : Trigger.new) {
String currentStage = opp.StageName;
Boolean stageChanged = false;
// Check if stage changed (for updates)
if (Trigger.isUpdate) {
Opportunity oldOpp = Trigger.oldMap.get(opp.Id);
stageChanged = (opp.StageName != oldOpp.StageName);
}
// Create task if new opportunity or stage changed
if (Trigger.isInsert || stageChanged) {
if (stageTaskMap.containsKey(currentStage)) {
Task newTask = new Task();
newTask.WhatId = opp.Id;
newTask.Subject = stageTaskMap.get(currentStage);
newTask.Priority = 'High';
newTask.Status = 'Not Started';
newTask.OwnerId = opp.OwnerId;
// Set due date based on stage
if (currentStage == 'Proposal/Price Quote') {
newTask.ActivityDate = Date.today().addDays(2);
} else if (currentStage == 'Negotiation/Review') {
newTask.ActivityDate = Date.today().addDays(5);
} else if (currentStage == 'Closed Won') {
newTask.ActivityDate = Date.today().addDays(1);
}
tasksToCreate.add(newTask);
}
}
}
// Bulk insert tasks
if (!tasksToCreate.isEmpty()) {
insert tasksToCreate;
}
}
Key Learning Points:
- Using Map for configuration
- Dynamic task creation
- Setting related fields (WhatId, OwnerId)
- Date calculations with addDays()
Example 10: Share Records with Team Members
Business Requirement: When an Opportunity amount exceeds $500K, automatically share it with the VP of Sales.
apextrigger OpportunityHighValueShare on Opportunity (after insert, after update) {
List<OpportunityShare> sharesToCreate = new List<OpportunityShare>();
// Get VP of Sales user ID (use custom setting in production)
User vpSales = [
SELECT Id
FROM User
WHERE Profile.Name = 'VP Sales'
AND IsActive = true
LIMIT 1
];
// Query existing shares to avoid duplicates
Set<Id> oppIdsToShare = new Set<Id>();
for (Opportunity opp : Trigger.new) {
Boolean amountChanged = false;
if (Trigger.isUpdate) {
Opportunity oldOpp = Trigger.oldMap.get(opp.Id);
amountChanged = (opp.Amount != oldOpp.Amount);
}
// Check if amount exceeds threshold
if (opp.Amount != null && opp.Amount >= 500000) {
// Only process if insert or amount changed
if (Trigger.isInsert || amountChanged) {
oppIdsToShare.add(opp.Id);
}
}
}
if (!oppIdsToShare.isEmpty()) {
// Check existing shares
Set<Id> alreadySharedOppIds = new Set<Id>();
for (OpportunityShare existingShare : [
SELECT OpportunityId
FROM OpportunityShare
WHERE OpportunityId IN :oppIdsToShare
AND UserOrGroupId = :vpSales.Id
]) {
alreadySharedOppIds.add(existingShare.OpportunityId);
}
// Create new shares
for (Id oppId : oppIdsToShare) {
if (!alreadySharedOppIds.contains(oppId)) {
OpportunityShare oppShare = new OpportunityShare();
oppShare.OpportunityId = oppId;
oppShare.UserOrGroupId = vpSales.Id;
oppShare.OpportunityAccessLevel = 'Read';
oppShare.RowCause = Schema.OpportunityShare.RowCause.Manual;
sharesToCreate.add(oppShare);
}
}
if (!sharesToCreate.isEmpty()) {
insert sharesToCreate;
}
}
}
Key Learning Points:
- Working with sharing objects
- Preventing duplicate shares
- Using Schema namespace
- Complex conditional logic
Trigger Handler Pattern (Advanced Architecture)
As your triggers grow in complexity, maintaining code becomes challenging. The Trigger Handler Pattern is an industry best practice that separates trigger logic into dedicated classes.
Why Use Trigger Handler Pattern?
✅ Better Organization: Logic separated by operation type
✅ Easier Testing: Test individual methods
✅ Reusability: Share logic across triggers
✅ Maintainability: Changes in one place
✅ Recursion Control: Built-in recursion prevention
Complete Trigger Handler Framework
Step 1: Create the Base Handler Interface
apexpublic interface ITriggerHandler {
void beforeInsert(List<SObject> newRecords);
void afterInsert(List<SObject> newRecords, Map<Id, SObject> newRecordsMap);
void beforeUpdate(List<SObject> newRecords, Map<Id, SObject> newRecordsMap, Map<Id, SObject> oldRecordsMap);
void afterUpdate(List<SObject> newRecords, Map<Id, SObject> newRecordsMap, Map<Id, SObject> oldRecordsMap);
void beforeDelete(List<SObject> oldRecords, Map<Id, SObject> oldRecordsMap);
void afterDelete(List<SObject> oldRecords, Map<Id, SObject> oldRecordsMap);
void afterUndelete(List<SObject> newRecords, Map<Id, SObject> newRecordsMap);
}
Step 2: Create the Trigger Handler Dispatcher
apexpublic virtual class TriggerHandler {
// Static map to prevent recursive calls
private static Map<String, Boolean> bypassedHandlers = new Map<String, Boolean>();
// Instance variables
protected String triggerContext;
public TriggerHandler() {
this.triggerContext = this.getHandlerName();
}
// Main method called from trigger
public void run() {
// Check if this handler should be bypassed
if (isBypassed()) {
return;
}
// Execute appropriate method based on trigger context
if (Trigger.isBefore) {
if (Trigger.isInsert) {
this.beforeInsert();
} else if (Trigger.isUpdate) {
this.beforeUpdate();
} else if (Trigger.isDelete) {
this.beforeDelete();
}
} else if (Trigger.isAfter) {
if (Trigger.isInsert) {
this.afterInsert();
} else if (Trigger.isUpdate) {
this.afterUpdate();
} else if (Trigger.isDelete) {
this.afterDelete();
} else if (Trigger.isUndelete) {
this.afterUndelete();
}
}
}
// Virtual methods to be overridden
protected virtual void beforeInsert() {}
protected virtual void beforeUpdate() {}
protected virtual void beforeDelete() {}
protected virtual void afterInsert() {}
protected virtual void afterUpdate() {}
protected virtual void afterDelete() {}
protected virtual void afterUndelete() {}
// Bypass mechanism
public static void bypass(String handlerName) {
bypassedHandlers.put(handlerName, true);
}
public static void clearBypass(String handlerName) {
bypassedHandlers.remove(handlerName);
}
public static void clearAllBypasses() {
bypassedHandlers.clear();
}
private Boolean isBypassed() {
return bypassedHandlers.containsKey(this.getHandlerName()) &&
bypassedHandlers.get(this.getHandlerName());
}
private String getHandlerName() {
return String.valueOf(this).substring(0, String.valueOf(this).indexOf(':'));
}
}
Step 3: Create Object-Specific Handler
apexpublic class AccountTriggerHandler extends TriggerHandler {
private List<Account> newRecords;
private List<Account> oldRecords;
private Map<Id, Account> newRecordsMap;
private Map<Id, Account> oldRecordsMap;
public AccountTriggerHandler() {
this.newRecords = (List<Account>) Trigger.new;
this.oldRecords = (List<Account>) Trigger.old;
this.newRecordsMap = (Map<Id, Account>) Trigger.newMap;
this.oldRecordsMap = (Map<Id, Account>) Trigger.oldMap;
}
protected override void beforeInsert() {
setDefaultRating(this.newRecords);
validateAccountData(this.newRecords);
}
protected override void beforeUpdate() {
validateAccountData(this.newRecords);
trackIndustryChanges(this.newRecords, this.oldRecordsMap);
}
protected override void afterInsert() {
createPrimaryContact(this.newRecords);
}
protected override void afterUpdate() {
updateRelatedOpportunities(this.newRecordsMap, this.oldRecordsMap);
}
protected override void beforeDelete() {
preventDeletionWithOpportunities(this.oldRecords);
}
// Business logic methods
private void setDefaultRating(List<Account> accounts) {
for (Account acc : accounts) {
if (acc.AnnualRevenue != null) {
if (acc.AnnualRevenue >= 10000000) {
acc.Rating = 'Hot';
} else if (acc.AnnualRevenue >= 5000000) {
acc.Rating = 'Warm';
} else {
acc.Rating = 'Cold';
}
}
}
}
private void validateAccountData(List<Account> accounts) {
for (Account acc : accounts) {
if (String.isBlank(acc.Phone) && String.isBlank(acc.Website)) {
acc.addError('Either Phone or Website must be provided.');
}
}
}
private void trackIndustryChanges(List<Account> accounts, Map<Id, Account> oldMap) {
for (Account acc : accounts) {
Account oldAcc = oldMap.get(acc.Id);
if (acc.Industry != oldAcc.Industry) {
acc.Industry_Changed_Date__c = System.today();
}
}
}
private void createPrimaryContact(List<Account> accounts) {
List<Contact> contactsToInsert = new List<Contact>();
for (Account acc : accounts) {
Contact con = new Contact();
con.AccountId = acc.Id;
con.LastName = acc.Name + ' - Primary';
con.Email = 'contact@' + acc.Name.replace(' ', '').toLowerCase() + '.com';
contactsToInsert.add(con);
}
if (!contactsToInsert.isEmpty()) {
insert contactsToInsert;
}
}
private void updateRelatedOpportunities(Map<Id, Account> newMap, Map<Id, Account> oldMap) {
Set<Id> accountIds = new Set<Id>();
for (Id accId : newMap.keySet()) {
Account newAcc = newMap.get(accId);
Account oldAcc = oldMap.get(accId);
if (newAcc.Rating != oldAcc.Rating) {
accountIds.add(accId);
}
}
if (!accountIds.isEmpty()) {
List<Opportunity> oppsToUpdate = [
SELECT Id, Account_Rating__c, AccountId
FROM Opportunity
WHERE AccountId IN :accountIds
];
for (Opportunity opp : oppsToUpdate) {
opp.Account_Rating__c = newMap.get(opp.AccountId).Rating;
}
update oppsToUpdate;
}
}
private void preventDeletionWithOpportunities(List<Account> accounts) {
Set<Id> accountIds = new Set<Id>();
for (Account acc : accounts) {
accountIds.add(acc.Id);
}
Map<Id, Account> accountsWithOpps = new Map<Id, Account>([
SELECT Id, (SELECT Id FROM Opportunities LIMIT 1)
FROM Account
WHERE Id IN :accountIds
]);
for (Account acc : accounts) {
if (accountsWithOpps.get(acc.Id).Opportunities.size() > 0) {
acc.addError('Cannot delete Account with related Opportunities.');
}
}
}
}
Step 4: Simplified Trigger
apextrigger AccountTrigger on Account (before insert, before update, after insert, after update, before delete) {
new AccountTriggerHandler().run();
}
Benefits of This Pattern:
- ✅ Clean, one-line trigger
- ✅ All logic organized in handler class
- ✅ Easy to test individual methods
- ✅ Built-in recursion prevention
- ✅ Ability to bypass handlers in tests
- ✅ Scalable for complex requirements
Testing Apex Triggers
Testing is mandatory in Salesforce. You need at least 75% code coverage to deploy to production. But good tests do more than just coverage—they verify your business logic works correctly.
Test Class Best Practices
apex@isTest
private class AccountTriggerHandlerTest {
// Test data setup method
@TestSetup
static void setupTestData() {
// Create test accounts
List<Account> testAccounts = new List<Account>();
for (Integer i = 0; i < 200; i++) {
Account acc = new Account();
acc.Name = 'Test Account ' + i;
acc.AnnualRevenue = 1000000 * i;
testAccounts.add(acc);
}
insert testAccounts;
}
// Test before insert logic
@isTest
static void testDefaultRatingAssignment() {
Test.startTest();
Account hotAccount = new Account(
Name = 'Hot Account',
AnnualRevenue = 15000000
);
Account warmAccount = new Account(
Name = 'Warm Account',
AnnualRevenue = 7000000
);
Account coldAccount = new Account(
Name = 'Cold Account',
AnnualRevenue = 2000000
);
List<Account> accounts = new List<Account>{
hotAccount, warmAccount, coldAccount
};
insert accounts;
Test.stopTest();
// Verify ratings
List<Account> insertedAccounts = [
SELECT Id, Rating, AnnualRevenue
FROM Account
WHERE Id IN :accounts
ORDER BY AnnualRevenue DESC
];
System.assertEquals('Hot', insertedAccounts[0].Rating, 'High revenue should be Hot');
System.assertEquals('Warm', insertedAccounts[1].Rating, 'Medium revenue should be Warm');
System.assertEquals('Cold', insertedAccounts[2].Rating, 'Low revenue should be Cold');
}
// Test bulk processing
@isTest
static void testBulkInsert() {
Test.startTest();
List<Account> bulkAccounts = new List<Account>();
for (Integer i = 0; i < 200; i++) {
bulkAccounts.add(new Account(
Name = 'Bulk Test ' + i,
AnnualRevenue = 5000000
));
}
insert bulkAccounts;
Test.stopTest();
// Verify all were processed
Integer accountCount = [SELECT COUNT() FROM Account WHERE Name LIKE 'Bulk Test%'];
System.assertEquals(200, accountCount, 'All 200 accounts should be inserted');
// Verify primary contacts created
Integer contactCount = [SELECT COUNT() FROM Contact WHERE Account.Name LIKE 'Bulk Test%'];
System.assertEquals(200, contactCount, 'Primary contacts should be created');
}
// Test update logic
@isTest
static void testIndustryChangeTracking() {
Account acc = [SELECT Id, Industry FROM Account LIMIT 1];
Test.startTest();
acc.Industry = 'Technology';
update acc;
Test.stopTest();
Account updatedAcc = [
SELECT Id, Industry_Changed_Date__c
FROM Account
WHERE Id = :acc.Id
];
System.assertEquals(System.today(), updatedAcc.Industry_Changed_Date__c,
'Industry change date should be set');
}
// Test validation logic
@isTest
static void testValidationError() {
Account invalidAccount = new Account(Name = 'Invalid Account');
// No phone or website
Test.startTest();
try {
insert invalidAccount;
System.assert(false, 'Should have thrown validation error');
} catch (DmlException e) {
System.assert(e.getMessage().contains('Either Phone or Website'),
'Should show validation message');
}
Test.stopTest();
}
// Test delete prevention
@isTest
static void testDeletionPrevention() {
Account acc = [SELECT Id FROM Account LIMIT 1];
// Create opportunity
Opportunity opp = new Opportunity(
Name = 'Test Opp',
AccountId = acc.Id,
StageName = 'Prospecting',
CloseDate = Date.today().addDays(30)
);
insert opp;
Test.startTest();
try {
delete acc;
System.assert(false, 'Should not allow deletion');
} catch (DmlException e) {
System.assert(e.getMessage().contains('Cannot delete'),
'Should prevent deletion');
}
Test.stopTest();
}
// Test bypass mechanism
@isTest
static void testHandlerBypass() {
Test.startTest();
// Bypass handler
TriggerHandler.bypass('AccountTriggerHandler');
Account acc = new Account(
Name = 'Bypass Test',
AnnualRevenue = 20000000
);
insert acc;
// Clear bypass
TriggerHandler.clearBypass('AccountTriggerHandler');
Test.stopTest();
Account insertedAcc = [SELECT Id, Rating FROM Account WHERE Id = :acc.Id];
// Rating should be null because handler was bypassed
System.assertEquals(null, insertedAcc.Rating, 'Rating should not be set when bypassed');
}
}
Key Testing Concepts
1. Test.startTest() and Test.stopTest()
- Resets governor limits
- Creates a separate execution context
- Essential for accurate testing
2. @TestSetup
- Runs once before all test methods
- Creates shared test data
- Improves test performance
3. Assertions
- System.assertEquals()
- System.assertNotEquals()
- System.assert()
4. Bulk Testing
- Always test with 200+ records
- Ensures your code is bulkified
- Catches governor limit issues
5. Negative Testing
- Test error conditions
- Test validation rules
- Use try-catch blocks
Debugging Apex Triggers
When things don’t work as expected, debugging skills are essential.
Debug Log Strategies
apextrigger DebugExample on Account (before insert) {
System.debug('=== TRIGGER START ===');
System.debug('Trigger.new size: ' + Trigger.new.size());
for (Account acc : Trigger.new) {
System.debug('Processing Account: ' + acc.Name);
System.debug('Annual Revenue: ' + acc.AnnualRevenue);
if (acc.AnnualRevenue != null && acc.AnnualRevenue >= 10000000) {
acc.Rating = 'Hot';
System.debug('Rating set to Hot');
}
}
System.debug('=== TRIGGER END ===');
}
Using Debug Levels
- Setup → Debug Logs
- Click New to create trace flag
- Set log levels:
- Apex Code: FINEST
- Database: INFO
- Workflow: INFO
- Validation: INFO
Common Debug Patterns
apex// Check if variable is null
System.debug('Value is null: ' + (myVariable == null));
// Log collection size
System.debug('List size: ' + myList.size());
// Log SOQL query results
System.debug('Query returned: ' + [SELECT COUNT() FROM Account]);
// Log execution time
Long startTime = System.currentTimeMillis();
// ... your code ...
Long endTime = System.currentTimeMillis();
System.debug('Execution time: ' + (endTime - startTime) + 'ms');
Performance Optimization
Writing triggers that perform well is crucial, especially when processing large data volumes.
Optimization Techniques
1. Selective Queries
apex// Bad - Queries all fields
List<Account> accounts = [SELECT FIELDS(ALL) FROM Account WHERE Id IN :accountIds];
// Good - Only query needed fields
List<Account> accounts = [SELECT Id, Name, Rating FROM Account WHERE Id IN :accountIds];
2. Use Maps for Lookups
apex// Bad - O(n²) complexity
for (Contact con : contacts) {
for (Account acc : accounts) {
if (con.AccountId == acc.Id) {
// Process
}
}
}
// Good - O(n) complexity
Map<Id, Account> accountMap = new Map<Id, Account>(accounts);
for (Contact con : contacts) {
Account acc = accountMap.get(con.AccountId);
if (acc != null) {
// Process
}
}
3. Limit Queries Inside Loops
apex// Bad
for (Opportunity opp : opportunities) {
List<OpportunityLineItem> items = [
SELECT Id FROM OpportunityLineItem WHERE OpportunityId = :opp.Id
];
}
// Good
Map<Id, Opportunity> oppMap = new Map<Id, Opportunity>([
SELECT Id, (SELECT Id FROM OpportunityLineItems)
FROM Opportunity
WHERE Id IN :opportunities
]);
4. Use Aggregate Queries
apex// Instead of loading all records
List<Opportunity> opps = [SELECT Amount FROM Opportunity WHERE AccountId = :accId];
Decimal total = 0;
for (Opportunity opp : opps) {
total += opp.Amount;
}
// Use aggregates
AggregateResult result = [
SELECT SUM(Amount) total
FROM Opportunity
WHERE AccountId = :accId
];
Decimal total = (Decimal)result.get('total');
Migration Guide: Process Builder to Apex Triggers
Many orgs are migrating from Process Builder to triggers for better performance and maintainability.
Example: Migrating a Process Builder
Old Process Builder Logic:
- When Opportunity Stage = “Closed Won”
- Create Task with specific details
New Trigger Implementation:
apextrigger OpportunityClosedWonTask on Opportunity (after update) {
List<Task> tasksToCreate = new List<Task>();
for (Opportunity opp : Trigger.new) {
Opportunity oldOpp = Trigger.oldMap.get(opp.Id);
// Check if stage changed to Closed Won
if (opp.StageName == 'Closed Won' && oldOpp.StageName != 'Closed Won') {
Task newTask = new Task();
newTask.WhatId = opp.Id;
newTask.OwnerId = opp.OwnerId;
newTask.Subject = 'Send welcome package';
newTask.Priority = 'High';
newTask.Status = 'Not Started';
newTask.ActivityDate = Date.today().addDays(1);
tasksToCreate.add(newTask);
}
}
if (!tasksToCreate.isEmpty()) {
insert tasksToCreate;
}
}
Benefits of Migration:
- ⚡ 5-10x faster execution
- 📊 Better debugging capabilities
- 🔄 Proper bulk processing
- 🧪 Testable with code coverage
Integration with External Systems
Triggers can call external APIs, but there are specific patterns to follow.
Calling External APIs from Triggers
apextrigger AccountExternalSync on Account (after insert, after update) {
Set<Id> accountIds = new Set<Id>();
for (Account acc : Trigger.new) {
if (Trigger.isInsert || acc.Name != Trigger.oldMap.get(acc.Id).Name) {
accountIds.add(acc.Id);
}
}
if (!accountIds.isEmpty()) {
// Call future method for external callout
ExternalSystemIntegration.syncAccounts(accountIds);
}
}
apexpublic class ExternalSystemIntegration {
@future(callout=true)
public static void syncAccounts(Set<Id> accountIds) {
List<Account> accounts = [
SELECT Id, Name, Industry, AnnualRevenue
FROM Account
WHERE Id IN :accountIds
];
for (Account acc : accounts) {
HttpRequest req = new HttpRequest();
req.setEndpoint('callout:External_System/api/accounts');
req.setMethod('POST');
req.setHeader('Content-Type', 'application/json');
Map<String, Object> payload = new Map<String, Object>{
'salesforce_id' => acc.Id,
'name' => acc.Name,
'industry' => acc.Industry,
'revenue' => acc.AnnualRevenue
};
req.setBody(JSON.serialize(payload));
Http http = new Http();
try {
HttpResponse res = http.send(req);
if (res.getStatusCode() == 200) {
System.debug('Successfully synced: ' + acc.Name);
} else {
System.debug('Error syncing: ' + res.getBody());
}
} catch (Exception e) {
System.debug('Exception: ' + e.getMessage());
}
}
}
}
Key Points:
- Use
@future(callout=true)for async callouts - Never make synchronous callouts from triggers
- Use Named Credentials for authentication
- Implement error handling
Governance and Limits
Understanding governor limits is critical for enterprise-grade triggers.
Critical Limits to Remember
| Resource | Synchronous Limit | Asynchronous Limit |
|---|---|---|
| Total SOQL queries | 100 | 200 |
| Total DML statements | 150 | 150 |
| Total records retrieved by SOQL | 50,000 | 50,000 |
| Total records processed by DML | 10,000 | 10,000 |
| Total heap size | 6 MB | 12 MB |
| Maximum CPU time | 10,000 ms | 60,000 ms |
Checking Limits in Code
apextrigger LimitMonitoring on Account (after insert) {
System.debug('SOQL Queries used: ' + Limits.getQueries() + ' / ' + Limits.getLimitQueries());
System.debug('DML Statements used: ' + Limits.getDmlStatements() + ' / ' + Limits.getLimitDmlStatements());
System.debug('Heap Size used: ' + Limits.getHeapSize() + ' / ' + Limits.getLimitHeapSize());
System.debug('CPU Time used: ' + Limits.getCpuTime() + ' / ' + Limits.getLimitCpuTime());
}
Common Mistakes to Avoid
1. DML Operations Inside Loops
Problem:
apex// WRONG
for (Contact con : Trigger.new) {
insert new Task(WhoId = con.Id);
}
Solution:
apex// CORRECT
List<Task> tasks = new List<Task>();
for (Contact con : Trigger.new) {
tasks.add(new Task(WhoId = con.Id));
}
insert tasks;
2. SOQL Inside Loops
Problem:
apex// WRONG
for (Opportunity opp : Trigger.new) {
Account acc = [SELECT Id FROM Account WHERE Id = :opp.AccountId];
}
Solution:
apex// CORRECT
Set<Id> accountIds = new Set<Id>();
for (Opportunity opp : Trigger.new) {
accountIds.add(opp.AccountId);
}
Map<Id, Account> accounts = new Map<Id, Account>([
SELECT Id FROM Account WHERE Id IN :accountIds
]);
3. Not Checking for Null Values
Problem:
apex// May cause NullPointerException
String email = acc.PersonEmail.toLowerCase();
Solution:
apex// Safe approach
String email = String.isNotBlank(acc.PersonEmail)
? acc.PersonEmail.toLowerCase()
: '';
4. Hardcoding IDs
Problem:
apex// WRONG - IDs differ between sandboxes and production
if (acc.RecordTypeId == '012000000000ABC') {
// logic
}
Solution:
apex// CORRECT - Query RecordType dynamically
Id corporateRT = Schema.SObjectType.Account
.getRecordTypeInfosByDeveloperName()
.get('Corporate').getRecordTypeId();
if (acc.RecordTypeId == corporateRT) {
// logic
}
5. Not Considering Field History Changes
When checking if a field changed, always compare old and new values:
apexif (Trigger.isUpdate) {
for (Opportunity opp : Trigger.new) {
Opportunity oldOpp = Trigger.oldMap.get(opp.Id);
// Check if stage actually changed
if (opp.StageName != oldOpp.StageName) {
// Process change
}
}
}
6. Ignoring Governor Limits
Always be mindful of:
- SOQL queries: 100 per transaction
- DML statements: 150 per transaction
- Heap size: 6 MB (synchronous), 12 MB (asynchronous)
- CPU time: 10,000 ms (synchronous)
Conclusion
Congratulations! You’ve completed this comprehensive Salesforce Apex triggers tutorial. You now understand:
What Apex triggers are and why they’re essential
Trigger structure and syntax Context variables and how to use them
Difference between before and after triggers
Real-world apex trigger examples you can implement today
Best practices that separate great developers from average ones
Common mistakes to avoid
Your Next Steps:
- Practice: Start with simple triggers and gradually tackle complex scenarios
- Test: Always write test classes for your triggers
- Refactor: Move logic to handler classes for better maintainability
- Learn: Explore trigger frameworks and advanced patterns
- Experiment: Try the examples in this guide in your Developer Org
Remember, mastering apex triggers beginner concepts is just the beginning. As you gain experience, you’ll discover more sophisticated patterns and techniques.
About RizeX Labs
At RizeX Labs, we empower aspiring developers with hands-on training in Salesforce and other emerging technologies. Our Salesforce programs focus on real-world development skills, including Apex, triggers, Lightning Web Components, and integrations.
We combine expert-led training, practical projects, and placement support to help learners become industry-ready Salesforce professionals.
Explore more Salesforce tutorials:
- Apex Best Practices
- Salesforce Integration Patterns
- Lightning Web Components Guide
- Salesforce Admin Certification Prep
Stay Connected:
Follow RizeX Labs for more Salesforce tips, tutorials, and real-world examples that accelerate your career growth.
Ready to Master Apex Triggers?
Start implementing these apex trigger examples in your Developer Org today. Remember, practice makes perfect. The more you code, the more confident you’ll become.
Got questions? Leave a comment below, and our team at RizeX Labs will be happy to help!
Happy Coding!
Internal Linking Opportunities
External Linking Opportunities
- Salesforce official website
- Salesforce Trailhead (free learning platform)
- Salesforce Developer Docs
- Salesforce Apex Developer Guide
Who Should Learn Apex Triggers?
- Beginners starting Salesforce development
- Salesforce Admins transitioning to Developer roles
- Developers preparing for Salesforce certifications
- Anyone looking to automate complex business processes
Next Steps
- Practice trigger examples in your Salesforce Developer Org
- Learn Apex classes and SOQL for deeper backend logic
- Explore trigger frameworks for scalable architecture
- Build real-world projects to strengthen your portfolio
Quick Summary
Salesforce Apex Triggers are a powerful way to automate business logic by executing code before or after database operations. In this beginner-friendly guide, you learned how triggers work, their structure, context variables, and explored real-time apex trigger examples like preventing duplicates, updating related records, and automating workflows. By following best practices like bulkification, using handler classes, and avoiding common mistakes, developers can build scalable and efficient Salesforce automation solutions.
