You Made It
Whew! Thirty episodes down. You are a machine.
Now it’s time for the big one. Triggers are where everything you’ve learned so far actually gets used, and they’re the single most common place you’ll write Apex in a real job. This is a long one, so grab a drink and let’s get to it.
What Is A Trigger?
A trigger is Apex that runs automatically when records change. You don’t call it. Somebody saves a record, and your code fires.
That’s it. Everything else is detail about when it fires and what you’re allowed to do at each point.
trigger ContactTrigger on Contact (before update, after update)
{
//your code here
}Three parts: the trigger’s name, the object it watches, and which events it responds to.
The Seven Events
before insert
before update
before delete
after insert
after update
after delete
after undeleteNote there’s no before undelete. Don’t go looking for it.
The before/after distinction is the thing to actually understand, because it decides what you can and can’t do:
Before events fire before the record is written to the database. The record has no Id yet on insert. You can change field values directly and they’ll be saved, without any extra DML. This is the cheap one.
After events fire once the record is committed. The record has an Id. Fields are read only, so if you want to change this record you need an explicit update, which costs you DML and can cause recursion.
Which leads to the rule that will save you the most grief:
Changing fields on the record that triggered the event? Use before. Doing anything with related records, or needing the Id? Use after.
I have lost count of the number of orgs I’ve walked into where somebody used an after trigger to update the record that fired it, burning DML and causing recursion, when a before trigger and one line would have done it for free.
Trigger Context Variables
Inside a trigger you get a set of free variables telling you what’s going on:
Trigger.new //List of the records as they will be / now are
Trigger.old //List of the records as they were before
Trigger.newMap //Map<Id, SObject> of the new versions
Trigger.oldMap //Map<Id, SObject> of the old versions
Trigger.isBefore //true if this is a before event
Trigger.isAfter //true if this is an after event
Trigger.isInsert //true if records are being inserted
Trigger.isUpdate //true if records are being updated
Trigger.isDelete //true if records are being deleted
Trigger.isUndelete //true if records are being undeleted
Trigger.size //how many records are in this transactionSome of these are null depending on context, and this catches people constantly:Trigger.old and Trigger.oldMap are null on insert. Obviously, since there was no previous version.Trigger.new is null on delete. The records are going away.Trigger.newMap is null in before insert, because the records don’t have Ids yet and you can’t key a map on nothing.
Get those wrong and you’ll hit a NullPointerException from episode 14 in a place you weren’t expecting one.
Comparing Old And New
The classic trigger job: “did this field actually change?” Here’s a real example that blocks anyone from editing a Contact’s birthday:
trigger ContactTrigger on Contact (before update)
{
for (Contact cont : Trigger.new)
{
Contact oldContact = Trigger.oldMap.get(cont.Id);
if (cont.Birthdate != oldContact.Birthdate)
{
cont.addError('You can't change the birthday, sorry bro');
}
}
}Two genuinely useful things happening here.Trigger.oldMap.get(cont.Id) is the standard way to pair up the old and new version of a record. Note it’s a Map lookup rather than a nested loop, exactly as episode 30 told you to.addError() stops the save and shows the user your message. It works on the whole record like this, or on a specific field with cont.Birthdate.addError(...), which puts the message right next to the field on the page.
This “only act if the field actually changed” pattern is worth internalising. Without it, your logic re-runs on every single save whether anything relevant changed or not, which is wasteful at best and recursive at worst.
One Trigger Per Object. One.
Salesforce lets you create as many triggers on an object as you like. Please don’t.
Here’s the problem: you cannot control the order multiple triggers on the same object run in. It’s not alphabetical. It’s not creation order. It’s undefined. So if you have three Account triggers and one depends on what another did, congratulations, you’ve built a race condition into your org and it will eventually behave differently in production than it did in your sandbox.
One trigger per object. Non-negotiable.
Keep Your Triggers Empty
Second rule, just as important: no business logic in the trigger file itself.
Triggers can’t be unit tested directly, can’t be reused, and can’t be extended. Everything you learned about small focused methods goes out the window the moment you start piling logic into a trigger file.
So your trigger should look like this. All of it:
trigger Case_Trigger on Case (before insert)
{
new Case_Trigger_Handler().run();
}One line. That’s a properly written trigger. Everything real happens in a handler class:
public with sharing class Case_Trigger_Handler extends TriggerHandler
{
public override void beforeInsert()
{
Case_Utility.updateCaseSubject(Trigger.new);
Case_Utility.createCase();
}
}That handler extends a base TriggerHandler class which does the routing, working out which context you’re in and calling the right method. Remember the protected keyword from episode 24? This is exactly the pattern it exists for.
This is called a trigger framework, and I’ve written a full post on implementing one. If you’re building anything real, use one. The most popular is Kevin O’Hara’s, and it’s excellent.
Triggers Are Always Bulk
This is the mistake that puts more Salesforce developers in incident calls than anything else, so let me be blunt about it.
Your trigger will receive up to 200 records at a time. Always. Write it that way from the first line.
It is very tempting to write this, because it works perfectly when you edit one record in the UI:
//Fine with one record. Catastrophic with 200.
trigger BadTrigger on Contact (after insert)
{
for (Contact cont : Trigger.new)
{
Account acct = [SELECT Id, Name FROM Account WHERE Id = :cont.AccountId];
acct.Description = 'Has a new contact';
update acct;
}
}A SOQL query and a DML statement inside a loop. Both of the cardinal sins from episode 30, in one trigger.
Import 200 contacts and this blows both the 100 query limit and the 150 DML limit, and the entire import rolls back.
Here it is written properly:
trigger GoodTrigger on Contact (after insert)
{
Set<Id> accountIds = new Set<Id>();
for (Contact cont : Trigger.new)
{
if (cont.AccountId != null)
{
accountIds.add(cont.AccountId);
}
}
if (accountIds.isEmpty()) { return; }
List<Account> accountsToUpdate = [SELECT Id, Description
FROM Account
WHERE Id IN :accountIds];
for (Account acct : accountsToUpdate)
{
acct.Description = 'Has a new contact';
}
update accountsToUpdate;
}One query. One DML statement. Works identically with 1 record or 200. That’s the whole game.
Recursion, And How To Stop It
Here’s a fun one. Your after update trigger on Account updates the Account. Which fires the after update trigger. Which updates the Account. Which fires…
Salesforce eventually stops you with Maximum trigger depth exceeded, but only after you’ve wasted a pile of limits and confused everybody.
The standard fix uses a static variable, and now you’ll see exactly why static being transaction scoped is such a good thing:
public class TriggerControl
{
public static Boolean accountTriggerHasRun = false;
}
trigger AccountTrigger on Account (after update)
{
if (TriggerControl.accountTriggerHasRun) { return; }
TriggerControl.accountTriggerHasRun = true;
//your logic, which now runs exactly once per transaction
}Because the flag is static, every bit of code in the transaction sees the same one. Because statics reset between transactions, the next save starts clean. Perfect fit.
Most trigger frameworks give you this for free, incidentally, which is another reason to use one.
A Kill Switch Is Worth Its Weight In Gold
One more piece of hard won advice. Build a way to turn your triggers off before you need it.
At some point you’ll need to do a data load of 500,000 records, and your triggers will make it take nine hours or fail entirely. If you didn’t build a bypass, your options are deploying a code change to production in a hurry or waiting.
A Custom Setting with a checkbox per object does it, and I’ve written up exactly how to build one. Do it early. You’ll thank yourself.
The Rules, In One Place
1. One trigger per object. Order is undefined otherwise.
2. No logic in the trigger. One line calling a handler.
3. Always bulkify. No SOQL or DML in loops, ever.
4. Before for the record itself, after for related records.
5. Guard against recursion with a static flag.
6. Only act when the field actually changed.
7. Build a bypass before you need it.
Follow those seven and your triggers will be better than a genuinely alarming share of the Apex running in production orgs today.
What’s Next
All the code from this episode is up on GitHub, and the official trigger documentation is here.
I’ve been writing SOQL queries in examples for about fifteen episodes now while promising to explain them properly later. In the next episode I finally deliver on that.
See you next time!
Get Coding With The Force Merch!!
We now have a redbubble store setup so you can buy cool Coding With The Force merchandise! Please check it out! Every purchase goes to supporting the blog and YouTube channel.
Get Shirts Here!
Get Cups, Artwork, Coffee Cups, Bags, Masks and more here!
Check Out More Coding With The Force Stuff!
If you liked this post make sure to follow us on all our social media outlets to stay as up to date as possible with everything!
Youtube
Patreon
Github
Facebook
Twitter
Instagram
Salesforce Development Books I Recommend
Advanced Apex Programming
Salesforce Lightning Platform Enterprise Architecture
Mastering Salesforce DevOps
Good Non-SF Specific Development Books:
Clean Code
Clean Architecture