Order of execution, context variables, the handler pattern, recursion, and the trigger scenarios interviewers ask you to write - with bulkified code.
By Bhanu Pratap SinghLast reviewed 2026-10-2317 min read7 questions5 Intermediate2 Advanced
Level
Senior Salesforce interviews almost always include a whiteboard trigger. They are not testing syntax - they are testing whether your code survives 200 records, a second save and an admin's flow on the same object. Every example here is bulkified.
Q1AdvancedTriggersOrder of execution
What is the order of execution when a record is saved?
1What it is
The fixed sequence Salesforce runs on every save: before-save flows and before triggers, validation and duplicate rules, the write to the database, after triggers, assignment and workflow rules, after-save flows, roll-ups and sharing - and only then the commit.
2Why it matters
When a trigger, a flow and a validation rule all touch the same record, the order decides who wins. Knowing it explains "my field update was overwritten", "my validation rule never fired" and "my trigger ran twice".
3Example
A workflow field update saves the record again, so before and after update triggers run one more time - a classic cause of double processing.
4In short - say this in the interview
Before-save flows, before triggers, validation and duplicate rules, the save, after triggers, assignment and workflow rules, after-save flows, roll-ups and sharing, then commit - and emails and async jobs after the commit.
Common mistake
Assuming validation rules run before your before trigger. They run after it - your trigger can change a field that the rule then checks.
Likely follow-ups
Which runs first, a before-save flow or a before trigger? (The flow.)
Why can a callout not happen in a trigger directly?
Q2IntermediateTriggersContext variables
What are trigger context variables, and when is each one available?
1What it is
Built-in variables that tell a trigger what is happening: Trigger.new and Trigger.newMap (the records being saved), Trigger.old and Trigger.oldMap (their previous values), and flags like Trigger.isInsert, Trigger.isBefore and Trigger.operationType.
2Why it matters
One trigger handles many events, and each variable exists only in some of them. Reading Trigger.oldMap in an insert, or Trigger.newMap in before insert, gives you null - not an error at compile time.
3Example
Variable
Insert
Update
Delete
Undelete
Trigger.new
Yes
Yes
No
Yes (after)
Trigger.newMap
After only (no Ids before)
Yes
No
Yes (after)
Trigger.old
No
Yes
Yes
No
Trigger.oldMap
No
Yes
Yes
No
Edit Trigger.new fields
Before only
Before only
-
No
4In short - say this in the interview
Trigger.new and newMap hold the new versions, Trigger.old and oldMap the previous ones. newMap does not exist in before insert because the records have no Ids yet, old and oldMap exist only for update and delete, and only before triggers can change Trigger.new.
Common mistake
Using Trigger.newMap in a before insert trigger. It is null - there are no Ids until the record is saved.
Likely follow-ups
How do you detect that a specific field changed? (Compare Trigger.new with Trigger.oldMap.)
What does Trigger.operationType return?
Q3IntermediateTriggersDesign
Why one trigger per object, and what is a trigger handler?
1What it is
The pattern of keeping a single trigger per object that does nothing but hand off to a handler class, where each event calls a named method.
2Why it matters
With several triggers on one object, Salesforce does not guarantee which runs first. One trigger makes the order explicit; a handler makes the logic testable, reusable and easy to switch off.
One trigger per object, because the order between several triggers is not guaranteed. The trigger only calls a handler class, which routes each event to a named method - so the logic is ordered, testable and easy to switch off.
Common mistake
Putting business logic straight into the trigger body. It cannot be reused or tested on its own.
Likely follow-ups
How would you let an admin switch the trigger off? (A custom setting or custom metadata flag checked in the handler.)
Why switch on Trigger.operationType rather than nested if statements?
Q4IntermediateTriggersRecursion
How do you stop a trigger from running recursively?
1What it is
Recursion is a trigger causing its own save again - an after trigger that updates the same object, or a workflow field update - so it runs again and again until Salesforce stops it at a depth of 16.
2Why it matters
Besides hitting the limit, recursion repeats work: emails sent twice, records created twice, tasks doubled. The fix has to stop the repeat without skipping records that genuinely need processing.
3Example
TriggerGuard.cls - remember what was handled · Apex
publicclassTriggerGuard {
privatestaticSet<Id> seen = newSet<Id>();
// Returns only the records not processed yet in this transaction.publicstaticList<SObject> firstTime(List<SObject> records) {
List<SObject> fresh = newList<SObject>();
for (SObject r : records) {
if (seen.add(r.Id)) fresh.add(r); // add() is false when already seen
}
return fresh;
}
}
4In short - say this in the interview
First avoid it: change the record's own fields in a before trigger, with no DML. When an after trigger must update the same object, keep a static Set of processed Ids and skip records already handled in this transaction.
Common mistake
A single static Boolean flag. It blocks the second chunk of 200 records in the same transaction, so some records are silently never processed.
Likely follow-ups
Why does a static variable survive between trigger runs? (It lives for the whole transaction.)
Why does a workflow field update cause the trigger to run again?
Q5IntermediateScenarioBulkification
Scenario: when an Account's phone changes, update the phone on all its Contacts.
1What it is
An after update trigger on Account that finds the accounts whose Phone actually changed, then updates their contacts in one query and one DML.
2Why it matters
It tests three things at once: detecting a real change with Trigger.oldMap, bulkifying across many accounts, and not doing work when nothing changed.
3Example
In AccountTriggerHandler (after update) · Apex
staticvoid syncContactPhones(Map<Id, Account> newMap, Map<Id, Account> oldMap) {
Set<Id> changed = newSet<Id>();
for (Account a : newMap.values()) {
if (a.Phone != oldMap.get(a.Id).Phone) changed.add(a.Id);
}
if (changed.isEmpty()) return; // nothing to do - no query at allList<Contact> toUpdate = newList<Contact>();
for (Contact c : [SELECTId, AccountIdFROMContactWHEREAccountIdIN :changed]) {
toUpdate.add(newContact(Id = c.Id, Phone = newMap.get(c.AccountId).Phone));
}
update toUpdate; // one DML for every contact
}
4In short - say this in the interview
After update on Account: compare Trigger.new with Trigger.oldMap to find accounts whose phone changed, query their contacts once with WHERE AccountId IN, set the new phone, and update the list once.
Common mistake
Updating contacts for every account in the batch, changed or not - wasted DML and needless triggers on Contact.
Likely follow-ups
Could this be a flow instead? (Yes - a record-triggered flow on Account with an Update Records element.)
What if an account has 20,000 contacts? (Move the update to Queueable or Batch Apex.)
Q6AdvancedScenarioRoll-up
Scenario: keep a count of Contacts on each Account (a roll-up over a lookup).
1What it is
An after trigger on Contact - insert, update, delete and undelete - that recounts contacts for every account touched, including the old account when a contact moves.
2Why it matters
A roll-up summary field only works on master-detail, and Contact to Account is a lookup. The interviewer is checking that you remember delete, undelete and re-parenting - the cases most answers miss.
3Example
ContactCount.trigger · Apex
triggerContactCountonContact (afterinsert, afterupdate, afterdelete, afterundelete) {
Set<Id> accountIds = newSet<Id>();
for (Contact c : Trigger.isDelete ? Trigger.old : Trigger.new) {
if (c.AccountId != null) accountIds.add(c.AccountId);
if (Trigger.isUpdate) {
Idbefore = Trigger.oldMap.get(c.Id).AccountId; // moved: the old one changes tooif (before != null && before != c.AccountId) accountIds.add(before);
}
}
if (accountIds.isEmpty()) return;
Map<Id, Integer> counts = newMap<Id, Integer>();
for (AggregateResult r : [SELECTAccountId accId, COUNT(Id) total FROMContactWHEREAccountIdIN :accountIds GROUP BY AccountId]) {
counts.put((Id) r.get('accId'), (Integer) r.get('total'));
}
List<Account> updates = newList<Account>();
for (Id accId : accountIds) {
Integer n = counts.containsKey(accId) ? counts.get(accId) : 0; // last one removed: zero
updates.add(newAccount(Id = accId, Contact_Count__c = n));
}
update updates;
}
4In short - say this in the interview
An after insert, update, delete and undelete trigger on Contact: collect every affected account Id - including the old one when a contact moves - recount with one aggregate query, and update those accounts once, setting zero where none remain.
Common mistake
Adding or subtracting 1 instead of recounting. A failed or partial save leaves the count wrong for good; an aggregate recount always corrects itself.
Likely follow-ups
Why after triggers, not before? (The contacts must be saved to be counted.)
How would you do it without code? (A record-triggered flow, or a lookup roll-up tool.)
Q7IntermediateScenarioValidation
Scenario: prevent deleting an Account that has open Opportunities.
1What it is
A before delete trigger on Account that finds accounts with open opportunities and calls addError on them, which cancels just those deletions.
2Why it matters
Validation rules do not run on delete, so this is a job for a trigger. Before delete is the last moment the record still exists to be protected.
3Example
In AccountTriggerHandler (before delete) · Apex
staticvoid blockDeleteWithOpenDeals(Map<Id, Account> oldMap) {
for (AggregateResult r : [SELECTAccountId accId FROMOpportunityWHEREAccountIdIN :oldMap.keySet() ANDIsClosed = false
GROUP BY AccountId]) {
oldMap.get((Id) r.get('accId'))
.addError('This account has open opportunities. Close or move them first.');
}
}
4In short - say this in the interview
A before delete trigger: query open opportunities for the accounts in Trigger.oldMap, grouped by account, and call addError on each account that has any. Only those deletions fail; the rest go through.
Common mistake
Trying to use a validation rule. They do not fire on delete.
Likely follow-ups
What does addError do to the rest of the batch with allOrNone=false?
Where else is addError useful?
Answers describe documented behaviour, checked against the sources listed. Platforms change between releases - confirm limits and versions before an interview.