Salesforce interview prep · Part 3 of 7

Apex Trigger Scenario Interview Questions

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

1Load the record and new values2System validation (UI saves)3Before-save record-triggered flows4Before triggers5Validation rules6Duplicate rules7Save to database (not committed)8After triggers9Assignment and auto-response rules10Workflow updates (triggers run again)11After-save flows and processes12Roll-up summaries, sharing recalculation13Commit14After commit: emails, async ApexHighlighted: the steps you write code or flows in. Everything runs in one transaction until step 13.

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

VariableInsertUpdateDeleteUndelete
Trigger.newYesYesNoYes (after)
Trigger.newMapAfter only (no Ids before)YesNoYes (after)
Trigger.oldNoYesYesNo
Trigger.oldMapNoYesYesNo
Edit Trigger.new fieldsBefore onlyBefore 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.

3Example

AccountTrigger.trigger · Apex
trigger AccountTrigger on Account (
    before insert, before update, before delete,
    after insert, after update, after delete, after undelete) {
    AccountTriggerHandler.run();
}
AccountTriggerHandler.cls · Apex
public with sharing class AccountTriggerHandler {
    public static void run() {
        switch on Trigger.operationType {
            when BEFORE_INSERT { setDefaults((List<Account>) Trigger.new); }
            when AFTER_UPDATE  { syncContactPhones((Map<Id, Account>) Trigger.newMap,
                                                   (Map<Id, Account>) Trigger.oldMap); }
            when BEFORE_DELETE { blockDeleteWithOpenDeals((Map<Id, Account>) Trigger.oldMap); }
        }
    }

    static void setDefaults(List<Account> accounts) { /* ... */ }
    static void syncContactPhones(Map<Id, Account> newMap, Map<Id, Account> oldMap) { /* ... */ }
    static void blockDeleteWithOpenDeals(Map<Id, Account> oldMap) { /* ... */ }
}

4In short - say this in the interview

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

Account is updatedAccountTrigger firesTrigger updates Accountrepeats again and againAfter 16 rounds: Maximum trigger depth exceeded
TriggerGuard.cls - remember what was handled · Apex
public class TriggerGuard {
    private static Set<Id> seen = new Set<Id>();

    // Returns only the records not processed yet in this transaction.
    public static List<SObject> firstTime(List<SObject> records) {
        List<SObject> fresh = new List<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
static void syncContactPhones(Map<Id, Account> newMap, Map<Id, Account> oldMap) {
    Set<Id> changed = new Set<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 all

    List<Contact> toUpdate = new List<Contact>();
    for (Contact c : [SELECT Id, AccountId FROM Contact WHERE AccountId IN :changed]) {
        toUpdate.add(new Contact(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
trigger ContactCount on Contact (after insert, after update, after delete, after undelete) {
    Set<Id> accountIds = new Set<Id>();
    for (Contact c : Trigger.isDelete ? Trigger.old : Trigger.new) {
        if (c.AccountId != null) accountIds.add(c.AccountId);
        if (Trigger.isUpdate) {
            Id before = Trigger.oldMap.get(c.Id).AccountId;   // moved: the old one changes too
            if (before != null && before != c.AccountId) accountIds.add(before);
        }
    }
    if (accountIds.isEmpty()) return;

    Map<Id, Integer> counts = new Map<Id, Integer>();
    for (AggregateResult r : [SELECT AccountId accId, COUNT(Id) total FROM Contact
                              WHERE AccountId IN :accountIds GROUP BY AccountId]) {
        counts.put((Id) r.get('accId'), (Integer) r.get('total'));
    }

    List<Account> updates = new List<Account>();
    for (Id accId : accountIds) {
        Integer n = counts.containsKey(accId) ? counts.get(accId) : 0;  // last one removed: zero
        updates.add(new Account(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
static void blockDeleteWithOpenDeals(Map<Id, Account> oldMap) {
    for (AggregateResult r : [SELECT AccountId accId FROM Opportunity
                              WHERE AccountId IN :oldMap.keySet() AND IsClosed = 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.