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
In a nutshell
  • A trigger handles up to 200 records per run; always write for bulk.
  • Before triggers change the records in Trigger.new without DML; after triggers see Ids and change other records.
  • Trigger.newMap is not available in before insert; Trigger.old is 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.

DML on recordsTriggerbefore or afterHandler classbulk logicCollectionsone query, one DML

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 variableHolds
Trigger.newList of new versions of the records (insert, update, undelete)
Trigger.oldList of old versions (update, delete)
Trigger.newMap / Trigger.oldMapMaps of Id to record; newMap is not available in before insert
Trigger.isBefore, isAfter, isInsert, isUpdate, isDelete, isUndeleteWhich event is running
Trigger.operationTypeAn enum such as BEFORE_INSERT for switch on
Trigger.sizeNumber of records in this run

Before versus after

Use before triggers toUse after triggers to
Change fields on the records being saved, with no DMLUse 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

PracticeWhy
One trigger per objectThe order of several triggers on one object is not guaranteed
Logic-less trigger, handler classEasier to test, reuse and control
No SOQL or DML in loopsStays under the 100 query and 150 DML statement limits
Use collections and mapsHandles 1 or 200 records the same way
Use addError() to reject recordsShows a message and blocks only that record; partial-success loads keep the rest
Guard against recursionTrack processed Ids in a static Set
Avoid hardcoded IdsIds differ between orgs and sandboxes
Declare sharingwith 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.

Exam tip

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.new without DML; after triggers see Ids and change other records.
  • Trigger.newMap is not available in before insert; Trigger.old is not available in insert.
  • One trigger per object, logic in a handler class.
  • addError() blocks one record and keeps partial-success loads working.
Watch out

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.old and oldMap are 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.