Salesforce Apex Trigger
Fundamentals
§ What are Apex Triggers?
Apex triggers allow you to perform custom actions before or after
database events occur on records in Salesforce.
They fire automatically on standard objects (like Account, Contact),
custom objects, and some standard child objects.
v Trigger Syntax:
Triggers are defined using a different syntax than classes:
trigger TriggerName on ObjectName (trigger_events) {
// code_block
}
You can specify multiple events in a comma-separated list (insert,
update, delete, undelete).
L Possible Trigger Events:
before insert
before update
before delete
after insert
after update
after delete
after undelete
Types of Triggers
Before Triggers
Used to update or validate record values before they are
saved to the database.
After Triggers
Work with data after the data is commited (saved) to the
database.
Used to access field values that are set by the system (like
Id or LastModifiedDate) and to affect changes in other
records. Records that fire an after trigger are read-only.
Using Context Variables
Context variables are used to access the records that caused the trigger to
fire.
Trigger.new
Contains a list of the new versions of the sObject records. This is
available in insert, update, and undelete triggers. Records in
Trigger.new can only be modified in before triggers.
Trigger.old
Returns a list of the old versions of the sObject records. This is
available in update and delete triggers.
Trigger.newMap & Trigger.oldMap
Maps of IDs to the new/old versions of the sObject records. Available in
various trigger contexts.
Trigger State Variables
Includes isExecuting, isInsert, isUpdate, isDelete, isBefore, isAfter,
isUndelete, operationType, and size.
trigger ContactTrigger on Contact (before update) {
if (Trigger.isBefore && Trigger.isUpdate) {
ContactTriggerHandler.handleBeforeUpdate(Trigger.new, Trigger.oldMap);
}
}
public class ContactTriggerHandler {
public static void handleBeforeUpdate(List<Contact> newList, Map<Id, Contact>
oldMap) {
for (Contact con : newList) {
Contact oldCon = oldMap.get(con.Id);
if (con.Email != oldCon.Email) {
con.Description = 'Email was changed from ' + oldCon.Email + ' to ' +
con.Email;
}
}
// Optional debug/logging for context awareness
System.debug('Trigger Context Info:');
System.debug('isExecuting: ' + Trigger.isExecuting);
System.debug('isBefore: ' + Trigger.isBefore);
System.debug('isUpdate: ' + Trigger.isUpdate);
System.debug('size: ' + Trigger.size);
}
}
Salesforce Apex Best
Practices
Best practices are widely accepted techniques or
approaches considered superior to alternatives,
often becoming standard ways of doing things.
Ignoring them can sometimes raise eyebrows.
Here are some best practices for Salesforce Apex:
1. Bulkify Your Code
Write your Apex code to efficiently handle multiple records at once, not just
single records. Triggers, for example, can receive up to 200 records in one
execution (e.g., via data loads or imports).
Why it matters:
Non-bulkified code:
Fails when processing many records
Causes errors like governor limit exceptions
Wastes platform resources and slows down performance
Bulkification:
Boosts performance
Lowers resource consumption
Ensures compliance with Salesforce governor limits, which exist to
protect the multitenant platform from inefficient code
trigger ContactTrigger on Contact (before insert) {
if (Trigger.isBefore && Trigger.isInsert) {
ContactTriggerHandler.handleBeforeInsert(Trigger.new);
}
}
public class ContactTriggerHandler {
public static void handleBeforeInsert(List<Contact> newContacts) {
Set<Id> accountIds = new Set<Id>();
for (Contact con : newContacts) {
if (con.AccountId != null) {
accountIds.add(con.AccountId);
}
}
if (accountIds.isEmpty()) {
return;
}
Map<Id, Account> accountsToUpdate = new Map<Id, Account>(
[SELECT Id, NumberOfEmployees FROM Account WHERE Id IN :accountIds]
);
for (Contact con : newContacts) {
if (con.AccountId != null && accountsToUpdate.containsKey(con.AccountId))
{
Account acc = accountsToUpdate.get(con.AccountId);
acc.NumberOfEmployees += 1;
}
}
if (!accountsToUpdate.isEmpty()) {
update accountsToUpdate.values();
}
}
}
2. Avoid DML/SOQL Queries in Loops
Never place DML operations (insert, update, delete) or SOQL queries inside for
loops.
Why it matters:
SOQL and DML are resource-intensive and strictly limited by Apex governor
limits:
100 SOQL queries (sync)
200 SOQL queries (async)
150 DML operations
Using them inside a loop can easily cause runtime exceptions in bulk
operations4especially in triggers.
' Best Practice:
Move SOQL outside the loop
Batch DML operations into a list and perform them after the loop
Bulkified DML (Good):
trigger DmlTriggerBulk on Account (after update) {
if (Trigger.isAfter && Trigger.isUpdate) {
DmlTriggerBulkHandler.handleAfterUpdate(Trigger.newMap.keySet());
}
}
public class DmlTriggerBulkHandler {
public static void handleAfterUpdate(Set<Id> accountIds) {
if (accountIds == null || accountIds.isEmpty()) {
return;
}
List<Opportunity> relatedOpps = [ SELECT Id, Name, Probability FROM
Opportunity
WHERE AccountId IN :accountIds ];
List<Opportunity> oppsToUpdate = new List<Opportunity>();
for (Opportunity opp : relatedOpps) {
if (opp.Probability >= 50 && opp.Probability < 100) {
opp.Description = 'New description for opportunity.';
oppsToUpdate.add(opp);
}
}
if (!oppsToUpdate.isEmpty()) {
update oppsToUpdate;
}
}
}
3. Avoid Hard-Coded IDs
What it means:
Never embed literal Salesforce IDs (e.g., Account, Record Type IDs)
directly in Apex code.
Why it matters:
IDs vary across orgs (sandbox vs. production). Hard-coding them
breaks your code after deployments or sandbox refreshes4especially
with Record Types, which get new IDs each time.
' How to do it properly:
For Record Types:
Use DeveloperName instead of the hard-coded ID:
Schema.SObjectType.Account.getRecordTypeInfosByDeveloperName().get('B
usiness').getRecordTypeId();
For fixed reference records:
Store IDs in Custom Metadata or Custom Settings, and retrieve
them dynamically. This allows seamless updates across
environments.
¦ Exception:
Hardcoding the Master Record Type ID is technically safe because it9s
constant4but even then, using metadata is safer and cleaner.
4. Explicitly Declare Sharing Model
What it means:
When writing Apex classes, always declare the sharing model using with
sharing, without sharing, or inherited sharing.
Why it matters:
Being explicit clarifies how the class handles record-level access:
with sharing: respects org-wide defaults and sharing rules.
without sharing: bypasses them.
inherited sharing: uses the context of the caller (recommended for
flexibility).
Omitting the keyword creates ambiguity and can lead to unexpected security
behavior.
' When to omit (rarely):
Only omit if your class never performs DML or SOQL. Even then, it9s safer to
declare it explicitly4preferably as inherited sharing for future-proofing.
public with sharing class MyClass {
// Code that respects sharing rules
}
public without sharing class MyUtility {
// Code that bypasses sharing rules
}
public inherited sharing class MyHandler {
// Inherits sharing from calling context
}
5. Use One Trigger per Object
What it means:
Only create one Apex trigger for each Salesforce object (e.g., one for Account,
one for Contact).
Why it matters:
When multiple triggers exist on the same object, Salesforce doesn9t
guarantee the order of execution. This leads to:
Unpredictable behavior
Difficult debugging
Broken logic if triggers depend on a specific execution order (e.g., one
trigger sets a value that another uses)
' Best Practice:
Consolidate logic into a single trigger and delegate to a handler class for
separation of concerns.
trigger AccountTrigger on Account (before insert, after insert,
before update, after update,
before delete, after delete,
after undelete) {
// Call handler methods based on operation type
if (Trigger.isBefore) {
if (Trigger.isInsert) {
AccountHandler.handleBeforeInsert(Trigger.new);
} else if (Trigger.isUpdate) {
AccountHandler.handleBeforeUpdate(Trigger.old, Trigger.new,
Trigger.oldMap, Trigger.newMap);
}
// Other operations...
}
// After trigger handling...
}
6. Use SOQL for Loops
What it means:
Place your SOQL query directly inside the for loop declaration instead of
storing the results in a variable.
Why it matters:
Apex has strict heap size limits. Querying large datasets into a list variable
can consume too much memory and crash your code in production.
Using a SOQL for loop processes records in smaller batches behind the
scenes, reducing memory usage and avoiding heap limit errors.
ä Example:
Example:
' Good (SOQL for loop):
for (Account acc : [SELECT Id, Name FROM Account]) {
// Process each account
}
o Bad (assigning to a list first):
List<Account> accounts = [SELECT Id, Name FROM Account];
for (Account acc : accounts) {
// Process each account
}
Note: In triggers, which already batch records (up to 200), the chunking
benefit of a SOQL for loop is less significant than in other Apex contexts.
However, the syntax can look more elegant.
When to potentially avoid: Avoid SOQL for loops for aggregate queries.
Aggregate queries don't support the underlying chunking mechanism and
will throw an exception if they return more than 2000 rows.
7. Modularize Your Code
Modularize Your Code
Place reusable logic into separate, self-contained classes and call
these methods from your triggers. This improves readability,
maintainability, testability, and accelerates development by creating
well-tested modules.
Delegate Business Logic
Avoid business logic directly in triggers. Instead, use triggers to call
dedicated handler classes (often called TriggerHandlers or
TriggerHelpers) that contain the business logic.
_ 1. Trigger (clean, no business logic inside):
trigger AccountTrigger on Account (before insert) {
if (Trigger.isBefore && Trigger.isInsert) {
AccountTriggerHandler.handleBeforeInsert(Trigger.new);
}
}
h 2. Handler Class (business logic separated):
public class AccountTriggerHandler {
public static void handleBeforeInsert(List<Account> newAccounts) {
for (Account acc : newAccounts) {
if (acc.Name != null && acc.Name.contains('Test')) {
acc.Inactive__c = true;
}
}
}
}
8. Go Beyond Code Coverage
What it means:
When writing Apex tests, don9t aim just for the required 75% coverage4focus
on testing different use cases and scenarios.
Why it matters:
Code coverage tells you that the code ran, but not how well it ran. Great tests
check:
If the code behaves as expected
In positive and negative scenarios
With different user profiles and data volumes
This helps catch bugs early and ensures system resilience as your org evolves.
' How to do it effectively:
Write multiple test methods for the same logic, targeting different
situations.
Use assertions to validate that expected outcomes occurred.
Manually fail the test (System.assert(false)) if expected behavior doesn't
happen.
Think in terms of functional testing, not just line execution.
@isTest
private class AccountTriggerTest {
@isTest
static void testPositiveScenario() {
// Test happy path
}
@isTest
static void testNegativeScenario() {
// Test error handling
}
@isTest
static void testBulkScenario() {
// Test with 200 records
}
}
9. Avoid Nested Loops
What it means:
Try to avoid using loops inside other loops when working with related data
sets.
Why it matters:
Nested loops increase cognitive complexity, making code harder to
understand, maintain, debug, and test. They can also hurt performance,
especially in bulk operations.
' How to avoid nested loops:
Use Maps or SOQL relationships to flatten data processing into a single
loop.
Extract inner loop logic into separate methods for better readability and
testability.
// Instead of:
for (Account a : accounts) {
for (Contact c : a.Contacts) {
// Complex logic here
}
}
// Better approach:
for (Account a : accounts) {
processContactsForAccount(a.Contacts);
}
private void processContactsForAccount(List contacts) {
for (Contact c : contacts) {
// Complex logic here
}
}
10. Have a Naming Convention
What it is:
Establish and follow consistent rules for naming variables, classes,
methods, and other code elements within your team or project.
Why it's important:
A consistent naming convention makes it easier for anyone on the team
(including your future self) to understand the code's purpose and
functionality, improving collaboration and maintenance. The specific
convention is less important than the consistency of following it.
i Examples of Common Naming
Conventions:
Classes ³ PascalCase:
Example: AccountHandler
Methods ³ camelCase:
Example: processAccounts
Variables ³ camelCase:
Example: accountList
Constants ³ ALL_CAPS:
Example: MAX_RECORDS
Triggers ³ ObjectNameTrigger:
Example: AccountTrigger
11. Avoid Returning JSON to
Lightning Components
When building @AuraEnabled methods for Lightning Components, it's
tempting to return complex data (like records or lists) as JSON strings:
@AuraEnabled(cacheable=true)
public static String getAccounts() {
return JSON.serialize([SELECT Id FROM Account]);
}
¦ However, this is considered an anti-pattern because:
It consumes extra heap memory.
It uses unnecessary CPU cycles to serialize data.
It can quickly hit governor limits, especially with large data sets.
It leads to poor component performance.
' Recommended approach: Return the actual object or list directly:
@AuraEnabled(cacheable=true)
public static List<Account> getAccounts() {
return [SELECT Id FROM Account];
}
Salesforce will automatically handle the serialization/deserialization
efficiently, outside governor limits, reducing overhead and improving
performance.
Calling a Class Method from a
Trigger
You can call public utility methods from other Apex classes from within a
trigger. This promotes code reuse, reduces trigger size, improves code
maintenance, and supports object-oriented programming principles.
EmailManager Class
public class EmailManager {
public static void sendMail(String address, String subject, String body) {
// Logic to send email...
Messaging.SingleEmailMessage mail = new Messaging.SingleEmailMessage();
String[] toAddresses = new String[] {address};
mail.setToAddresses(toAddresses);
mail.setSubject(subject);
mail.setPlainTextBody(body);
Messaging.SendEmailResult[] results =
Messaging.sendEmail(
new Messaging.SingleEmailMessage[] { mail }
);
// Helper method to inspect results...
}
}
ExampleTrigger
trigger ExampleTrigger on Contact (after insert, after delete) {
if (Trigger.isInsert) {
Integer recordCount = Trigger.new.size();
// Call a utility method from another class
EmailManager.sendMail(
'Your email address',
'Trailhead Trigger Tutorial',
recordCount + ' contact(s) were inserted.'
);
} else if (Trigger.isDelete) {
// Process after delete
}
}
Using Trigger Exceptions
(addError())
In Apex triggers, you can prevent records from being saved by calling the
addError() method on a specific record.
It throws a fatal error, shows an error message in the UI, and logs the
issue.
If used during DML operations, addError() rolls back the entire transaction
(unless handling partial success in bulk DML).
When triggered by Apex DML, the system still processes all records to
compile a full list of errors.
' Example: Preventing Account Deletion
trigger ContactTrigger on Contact (before insert, before update) {
if (Trigger.isBefore && (Trigger.isInsert || Trigger.isUpdate)) {
ContactTriggerHandler.handleBeforeInsertOrUpdate(Trigger.new);
}
}
public class ContactTriggerHandler {
public static void handleBeforeInsertOrUpdate(List<Contact> newContacts) {
for (Contact con : newContacts) {
if (String.isBlank(con.Email)) {
con.addError('Email is required for all Contacts.');
}
}
}
}
Triggers and Callouts
Apex allows integration with external web services through callouts. When
making a callout from a trigger, it must be done asynchronously so the
trigger doesn't block operations while waiting for the external service's
response. Asynchronous callouts run in a background process.
_ To make an asynchronous callout from a trigger, call a method in a class
that is annotated with @future(callout=true).
CalloutClass
public class CalloutClass {
// @future annotation makes this method run asynchronously
// callout=true is required for methods making callouts
@future(callout=true)
public static void makeCallout() {
HttpRequest request = new HttpRequest();
String endpoint = 'http://yourHost/yourService'; // Hypothetical endpoint
request.setEndPoint(endpoint);
request.setMethod('GET');
HttpResponse response = new HTTP().send(request);
// Process response...
}
}
CalloutTrigger
trigger CalloutTrigger on Account (before insert, before update) {
// Call the asynchronous method
CalloutClass.makeCallout();
}
' Note that a valid endpoint URL and adding a remote site in Salesforce for
the endpoint are required for this to work.
These topics and best practices are vital for writing efficient, maintainable,
and robust Apex code, particularly in triggers.
# Resources &
Information
Official Salesforce Documentation
Apex Developer Guide
Triggers and Order of Execution
SOQL and SOSL Reference
Governor Limits
Best Practices for Triggers
por Carmen Solis