Process Automation and Logic · Topic 4 of 7
Apex Triggers and Class Best Practices
Apex triggers for Platform Developer: events, context variables, before versus after, bulkification, one trigger per object, handler classes and addError.
- 4 min
- A trigger handles up to 200 records per run; always write for bulk.
- Before triggers change the records in
Trigger.newwithout DML; after triggers see Ids and change other records. Trigger.newMapis not available in before insert;Trigger.oldis not available in insert.
What it is
An Apex trigger is code that runs before or after records of an object are inserted, updated, deleted or undeleted. Triggers fire for saves from almost every source: the UI, the API, Data Loader, flows and other Apex.
Because a trigger can receive up to 200 records at a time, every trigger must be written for bulk. Best practice also keeps the logic out of the trigger body, in a handler class.
How it works
Trigger events and context variables
trigger OpportunityTrigger on Opportunity (before insert, before update, after insert, after update) {
OpportunityTriggerHandler.run(Trigger.operationType, Trigger.new, Trigger.oldMap);
}
| Context variable | Holds |
|---|---|
Trigger.new | List of new versions of the records (insert, update, undelete) |
Trigger.old | List of old versions (update, delete) |
Trigger.newMap / Trigger.oldMap | Maps of Id to record; newMap is not available in before insert |
Trigger.isBefore, isAfter, isInsert, isUpdate, isDelete, isUndelete | Which event is running |
Trigger.operationType | An enum such as BEFORE_INSERT for switch on |
Trigger.size | Number of records in this run |
Before versus after
| Use before triggers to | Use after triggers to |
|---|---|
| Change fields on the records being saved, with no DML | Use the record Id (not yet set in before insert) |
Validate data and call addError() | Create or update related records |
| Read system fields set on save |
In before triggers you change Trigger.new directly. In after triggers the records in Trigger.new are read-only; changing them throws an exception, so update copies with DML.
Bulkified trigger code
public with sharing class ContactTriggerHandler {
public static void setAccountName(List<Contact> contacts) {
Set<Id> accIds = new Set<Id>();
for (Contact c : contacts) {
if (c.AccountId != null) accIds.add(c.AccountId);
}
Map<Id, Account> accs = new Map<Id, Account>(
[SELECT Name FROM Account WHERE Id IN :accIds]);
for (Contact c : contacts) {
Account a = accs.get(c.AccountId);
if (a != null) c.Description = a.Name;
}
}
}
One query for all records, a map for lookups, and no DML because this runs before insert.
Best practices
| Practice | Why |
|---|---|
| One trigger per object | The order of several triggers on one object is not guaranteed |
| Logic-less trigger, handler class | Easier to test, reuse and control |
| No SOQL or DML in loops | Stays under the 100 query and 150 DML statement limits |
| Use collections and maps | Handles 1 or 200 records the same way |
Use addError() to reject records | Shows a message and blocks only that record; partial-success loads keep the rest |
| Guard against recursion | Track processed Ids in a static Set |
| Avoid hardcoded Ids | Ids differ between orgs and sandboxes |
| Declare sharing | with sharing or inherited sharing on handlers unless there is a reason not to |
Rejecting records
for (Contact c : Trigger.new) {
if (String.isBlank(c.Email)) {
c.Email.addError('Email is required for this record type.');
}
}
addError() on a record or field marks that record as failed. With partial success (Data Loader, Database.insert(recs, false)), the other records still save.
A worked example
A trigger on Account calls a @future method once per account. A load of 200 accounts fails because a transaction can make only 50 future calls. The fix is to collect all Account Ids in a Set<Id> and make one @future call with the set. The same pattern applies to callouts, emails and queueable jobs: one call for the batch, not one per record.
When a code sample shows for (... : Trigger.new) with [SELECT ...] or update inside the loop, the expected answer is to bulkify: collect, query once, update once.
How the exam asks about this
- "Change a field on the record being saved." A before trigger, no DML.
- "Create a child record that needs the new record's Id." An after insert trigger.
- "Block saving and show a message on the field."
addError()in a before trigger.
What to remember for the exam
- A trigger handles up to 200 records per run; always write for bulk.
- Before triggers change the records in
Trigger.newwithout DML; after triggers see Ids and change other records. Trigger.newMapis not available in before insert;Trigger.oldis not available in insert.- One trigger per object, logic in a handler class.
addError()blocks one record and keeps partial-success loads working.
Updating the same records with DML inside their own after update trigger fires the trigger again and can loop until the depth limit. Change fields in a before trigger instead.
Common mistakes
Using DML to change Trigger.new in a before trigger. Just set the fields; the platform saves them.
Several triggers on one object. Their order is not guaranteed.
Throwing an exception to reject one record in a bulk load. An unhandled exception fails all records; addError() fails only the bad ones.
Check yourself
Why should a trigger avoid SOQL inside a loop over Trigger.new?
A 200-record load would run 200 queries and exceed the 100 SOQL query limit.
Which context variable is not available in a before insert trigger?
Trigger.newMap, because the records have no Ids yet (Trigger.oldandoldMapare also unavailable).How do you reject only the invalid records in a bulk insert?
Call
addError()on each invalid record or field.
Topic 4 of 7 in Process Automation and Logic
Test yourself on Process Automation and Logic
The full Salesforce Platform Developer question bank has 645 questions, with timed practice exams and an explanation for every answer.
This study guide is independent and is not affiliated with, endorsed by, or sponsored by Salesforce. Vendor names, certification names and exam codes are trademarks of their respective owners and are used for identification only.