Salesforce Interview Questions & Answers Book is now available!

Grab Now

Interview questions

Apex Trigger Interview Questions and Answers (2026)

Apex trigger interview questions with clear answers: context variables, order of execution, recursion, handler frameworks, rollups and flow conflicts.

By Teja, ex-Salesforce engineer · Updated October 6, 2026 · 13 min read

TL;DR

  • Trigger questions test four things: bulk safety, knowing which context variables exist in which event, the order of execution, and how you stop recursion without breaking bulk loads.
  • A static Boolean recursion guard is a trap: every 200-record chunk of one DML runs in the same transaction, so the flag silently skips chunks two onwards. Compare old and new values instead.
  • Before-save flows run before before triggers, and after-save flows run after after triggers. Know that order before you mix flows and triggers on one object.

These are the Apex trigger questions that come up in Salesforce developer interviews in 2026, with answers that explain why. Triggers are where interviewers test whether you can write code that survives real data: 200 records at a time, data loads, recursion and other automation on the same object.

The questions are grouped by level. If you're new to triggers, work through Fundamentals first. If you've written triggers in production, go straight to the order of execution, recursion and the scenarios. Answer each one out loud before you read mine.

Fundamentals

When do you use a before trigger, and when an after trigger?

Use a before trigger to change or validate the records being saved. You set fields directly on Trigger.new and they're saved with the record, with no DML and no extra trigger run. Defaulting fields, normalising values and blocking bad data belong here.

Use an after trigger when you need something that only exists once the record is saved: its Id (on insert), or system fields such as CreatedDate. After triggers are also where you create or update other records, such as related tasks, child records or a parent rollup.

The common mistake is using an after trigger to update the same records that fired it. That needs an extra DML, re-fires the update trigger and wastes limits. If the change is to the triggering record, it belongs in before.

What the interviewer is really checking: that you know before-trigger field changes are free.

Which trigger events exist?

Seven: before insert, after insert, before update, after update, before delete, after delete and after undelete. There's no before undelete.

trigger ContactTrigger on Contact (
    before insert, before update, before delete,
    after insert, after update, after delete, after undelete
) {
    ContactTriggerHandler.run();
}

Declare only the events you handle. Every declared event runs the trigger, even when the handler has nothing to do for it. Undelete is the one people forget: if you maintain a rollup count, restoring a record from the recycle bin has to add it back, and only after undelete tells you that happened.

Which context variables are available in which events?

VariableInsertUpdateDeleteUndelete
Trigger.newbefore (editable) and afterbefore (editable) and afternot availableafter
Trigger.newMapafter onlybefore and afternot availableafter
Trigger.oldnot availablebefore and afterbefore and afternot available
Trigger.oldMapnot availablebefore and afterbefore and afternot available

The logic behind it: newMap is keyed by Id, so it can't exist in before insert, when the records have no Ids yet. old values only exist for records that already existed. And Trigger.new can only be edited in before triggers. Changing a field on it in an after trigger throws a "record is read-only" error.

Also useful: Trigger.operationType, an enum (BEFORE_INSERT, AFTER_UPDATE and so on) that makes handler code a clean switch, and Trigger.size.

How many records does a trigger receive at once?

Up to 200. When a single DML operation includes more records, the platform runs the trigger in chunks of 200. An Apex insert of 1,000 records runs your trigger five times, and all five run in the same transaction, sharing the same governor limits and the same static variables.

This is why "bulkified" means more than "works for 200". If your trigger runs two queries per invocation, a 1,000-record insert costs ten queries before any other automation runs. And any static variable you set in the first chunk is still set in the fifth.

Test with more than 200 records, such as 250 or 401, so your tests cross a chunk boundary and catch bugs that only appear on the second chunk.

Why do people say "one trigger per object"?

Because if an object has several triggers for the same event, Salesforce doesn't guarantee the order they run in. Two triggers that both touch the same field, or where one depends on the other's result, will behave inconsistently, and the bug won't reproduce reliably.

With one trigger per object that delegates to a handler, the order is explicit in code. You can read the handler and know exactly what runs, in what sequence.

It also gives you one place for cross-cutting concerns: a bypass switch for data loads, recursion control and logging. Managed packages may add their own triggers to your objects, and you can't control those, which is one more reason to keep your own side predictable.

How does addError work in a trigger?

Calling addError() on a record in Trigger.new (or Trigger.old in delete triggers) marks that record as failed. The record isn't saved and the message is shown to the user or returned to the API caller. You can attach the error to a specific field so it appears next to that field in the UI:

for (Opportunity opp : (List<Opportunity>) Trigger.new) {
    if (opp.StageName == 'Closed Won' && opp.Amount == null) {
        opp.Amount.addError('Enter an amount before closing the opportunity.');
    }
}

Use it in before triggers for validation that a validation rule can't express, for example because it needs a query. In before delete, it's how you block a deletion. It also works in after triggers, where it rolls that record back, but by then you've done work for nothing. Note that with all-or-nothing DML, one record's error fails the whole operation. With partial success, only that record fails.

Intermediate

Explain the order of execution when a record is saved.

The parts interviewers care about, in order:

  1. The original record is loaded (or a new one initialised) and the new values are applied. Saves from a standard UI edit page run system validation, such as required fields and field formats.
  2. Before-save record-triggered flows run.
  3. Before triggers run.
  4. System validation runs again, then custom validation rules.
  5. Duplicate rules run.
  6. The record is saved to the database, but not committed.
  7. After triggers run.
  8. Assignment rules, auto-response rules and any legacy workflow rules run. A workflow field update saves the record again and fires before and after update triggers one more time.
  9. Escalation rules, then after-save record-triggered flows (and any legacy processes).
  10. Entitlement rules, then roll-up summary fields on the parent, which run the parent through its own save.
  11. Criteria-based sharing is evaluated.
  12. Everything is committed.
  13. Post-commit work runs: emails are sent, and enqueued async Apex and the async paths of flows start.

What the interviewer is really checking: points 2 and 3 (flows before triggers), and that async work only starts after commit.

Why do triggers recurse, and how do you prevent it properly?

A trigger recurses when its own work causes it to fire again: an after-update trigger updates records of the same object, a workflow field update re-saves the record, or a parent rollup updates children.

The textbook fix, a static Boolean that says "already ran", is a trap. All chunks of one DML share the transaction, so on a 1,000-record update the flag set in chunk one makes chunks two to five skip the logic. That's silent data corruption.

Better approaches, usually combined:

  • Act only on real changes. Compare Trigger.new with Trigger.oldMap and process only records whose relevant field changed. A second pass then finds nothing to do.
  • Track processed records, not a global flag, in a static Set<Id> for logic that must run once per record per transaction.
for (Opportunity opp : newList) {
    Opportunity previous = oldMap.get(opp.Id);
    if (opp.IsWon && !previous.IsWon && !processedIds.contains(opp.Id)) {
        wonNow.add(opp);
        processedIds.add(opp.Id);
    }
}

Why both? When a workflow field update re-fires the trigger, Trigger.old still holds the values from before the original save, so a change check alone would pass twice.

What does a trigger handler framework look like?

The trigger has no logic. It calls a handler, and the handler routes each event to methods that call services:

trigger OpportunityTrigger on Opportunity (before insert, before update, after update) {
    OpportunityTriggerHandler.run();
}
public with sharing class OpportunityTriggerHandler {
    public static void run() {
        if (FeatureManagement.checkPermission('Bypass_Opportunity_Automation')) {
            return;
        }
        switch on Trigger.operationType {
            when BEFORE_INSERT {
                OpportunityDefaults.apply((List<Opportunity>) Trigger.new);
            }
            when BEFORE_UPDATE {
                OpportunityValidation.validate((List<Opportunity>) Trigger.new, (Map<Id, Opportunity>) Trigger.oldMap);
            }
            when AFTER_UPDATE {
                OnboardingService.createKickoffTasks((List<Opportunity>) Trigger.new, (Map<Id, Opportunity>) Trigger.oldMap);
            }
        }
    }
}

What this buys you: the order of logic is explicit, every piece is testable on its own with plain lists, there's one bypass switch, and adding a feature means adding a method, not another trigger. The free Apex Triggers Framework 101 course builds one from scratch.

Can you make a callout from a trigger?

Not synchronously. A trigger runs inside a transaction that holds database locks, and Salesforce won't let it wait on an external system. Attempting it throws a CalloutException.

You hand the callout to asynchronous Apex, which runs after the transaction commits:

public with sharing class AccountSyncQueueable implements Queueable, Database.AllowsCallouts {
    private final Set<Id> accountIds;

    public AccountSyncQueueable(Set<Id> accountIds) {
        this.accountIds = accountIds;
    }

    public void execute(QueueableContext context) {
        List<Account> accounts = [SELECT Id, Name, BillingCountry FROM Account WHERE Id IN :accountIds];
        // build and send the request, then record the outcome
    }
}

From the trigger handler, enqueue it once per invocation with all the Ids, not once per record. A @future(callout=true) method works too, but a Queueable gives you a job Id, chaining and finalizers for retries. Platform events are the other option when an external system should subscribe instead.

Which operations don't fire triggers, and what happens on merge?

Some platform operations skip triggers. The one interviewers ask about is cascade delete: when you delete a parent in a master-detail relationship, the child records that are deleted with it don't fire their delete triggers. Only records that initiated the delete do. If you keep a count or audit log of child deletions, you have to handle it from the parent's delete trigger. Renaming or replacing picklist values doesn't fire triggers either.

Merge is the other favourite. When you merge records:

  • the losing records fire before and after delete triggers,
  • the winning record fires before and after update triggers,
  • in the after delete trigger, each losing record's MasterRecordId is set to the winner's Id.

That last point is how you move data from the losers to the winner, or log the merge.

What the interviewer is really checking: whether you've thought about data that changes without your trigger seeing it.

Scenario-based

Write a trigger that keeps a count of contacts on each account.

The trap is handling only insert. A correct answer covers insert, delete, undelete and reparenting, where an update moves a contact from one account to another and both accounts change.

trigger ContactTrigger on Contact (after insert, after update, after delete, after undelete) {
    Set<Id> accountIds = new Set<Id>();
    if (Trigger.new != null) {
        for (Contact c : (List<Contact>) Trigger.new) {
            accountIds.add(c.AccountId);
        }
    }
    if (Trigger.old != null) {
        for (Contact c : (List<Contact>) Trigger.old) {
            accountIds.add(c.AccountId);
        }
    }
    ContactCountRollup.recalculate(accountIds);
}
public without sharing class ContactCountRollup {
    // without sharing on purpose: the count must include contacts the running user can't see.
    public static void recalculate(Set<Id> accountIds) {
        accountIds.remove(null);
        if (accountIds.isEmpty()) {
            return;
        }
        Map<Id, Integer> counts = new Map<Id, Integer>();
        for (Id accountId : accountIds) {
            counts.put(accountId, 0);
        }
        for (AggregateResult row : [
            SELECT AccountId, COUNT(Id) total FROM Contact
            WHERE AccountId IN :accountIds GROUP BY AccountId
        ]) {
            counts.put((Id) row.get('AccountId'), (Integer) row.get('total'));
        }
        List<Account> updates = new List<Account>();
        for (Id accountId : counts.keySet()) {
            updates.add(new Account(Id = accountId, Contact_Count__c = counts.get(accountId)));
        }
        update updates;
    }
}

Recounting with an aggregate query is safer than adding and subtracting, because it corrects itself. In a real org I'd move the Id collection into a handler and check that AccountId actually changed on update. In an interview, say that out loud.

A before-save flow and a before trigger both set the same field. Which value is saved?

The trigger's. Before-save record-triggered flows run before before triggers, so the trigger runs later and overwrites the flow's value. On the after side, after-save flows run after after triggers.

It's a good answer to then say why this shouldn't happen in the first place. Two tools writing the same field means nobody can predict the result without knowing the order of execution by heart. I'd pick one owner for each field:

  • simple field defaults and updates on the same record: a before-save flow, which admins can maintain,
  • complex logic, heavy volumes or anything needing precise control: the trigger.

Flow Trigger Explorer shows every record-triggered flow on an object, and each flow's trigger order setting controls the order between flows. Neither puts a flow between your triggers, so ordering across the two tools comes only from the order of execution.

Block duplicate contacts by email, including duplicates inside the same data load.

Two checks are needed: against existing records, and within the batch itself, because two new contacts with the same email in one load won't find each other in the database.

public without sharing class ContactDuplicateGuard {
    // without sharing on purpose: a duplicate the user can't see is still a duplicate.
    public static void blockDuplicateEmails(List<Contact> contacts) {
        Map<String, Contact> byEmail = new Map<String, Contact>();
        for (Contact c : contacts) {
            if (String.isBlank(c.Email)) {
                continue;
            }
            String key = c.Email.toLowerCase();
            if (byEmail.containsKey(key)) {
                c.Email.addError('Another contact in this load uses this email.');
            } else {
                byEmail.put(key, c);
            }
        }
        if (byEmail.isEmpty()) {
            return;
        }
        for (Contact existing : [SELECT Id, Email FROM Contact WHERE Email IN :byEmail.keySet()]) {
            Contact incoming = byEmail.get(existing.Email.toLowerCase());
            if (incoming != null && incoming.Id != existing.Id) {
                incoming.Email.addError('A contact with this email already exists.');
            }
        }
    }
}

Call it from before insert and before update. Lower-casing the keys matters because a Map with String keys is case-sensitive. Then say the senior part: for many orgs, a standard duplicate rule, or a unique field, is less code to maintain. The trigger earns its place only when the matching logic is beyond those.

Send a notification only when a case is closed, not every time it's edited.

Compare old and new values in an after update trigger, and act only on the transition:

public with sharing class CaseClosedNotifier {
    public static void notifyOnClose(List<Case> newList, Map<Id, Case> oldMap) {
        Set<Id> closedNow = new Set<Id>();
        for (Case c : newList) {
            if (c.IsClosed && !oldMap.get(c.Id).IsClosed) {
                closedNow.add(c.Id);
            }
        }
        if (!closedNow.isEmpty()) {
            System.enqueueJob(new CaseClosedNotificationJob(closedNow));
        }
    }
}

Three things the interviewer wants to hear. Without the comparison, every later edit to a closed case sends another notification. The comparison also keeps the logic safe when the trigger fires again in the same transaction. And the work is collected into one async job for the whole chunk, not one per record, so a mass update of 200 cases enqueues one job, not 200.

Your trigger works in the UI but fails when someone loads 10,000 records with Data Loader. Why?

In the UI, people save one record at a time, so the trigger runs with one record. Data Loader sends records in batches, so the trigger runs with up to 200 records per invocation. Code that's fine for one record breaks at 200:

  • SOQL or DML inside a loop hits 100 queries or 150 DML statements.
  • Per-record async calls, such as one System.enqueueJob per record, hit the async limits.
  • Lock contention: many records pointing to the same parent, all updating it at once, can produce UNABLE_TO_LOCK_ROW errors.
  • Recursion guards built on a static Boolean quietly skip records when one DML spans several chunks.

The fix is the bulk pattern: collect Ids, query once, use maps, do one DML per object. Then prove it with a test that inserts more than 200 records.

Senior / design

How do you let a data migration bypass trigger logic safely?

Build the bypass into the handler, controlled by configuration instead of code changes:

  • A custom permission, such as Bypass_Opportunity_Automation, checked with FeatureManagement.checkPermission(). Assign it through a permission set to the migration user only, and remove it afterwards.
  • Custom metadata with a switch per object or per handler, for turning off one feature during an incident, deployable like any other metadata.
  • A hierarchy custom setting works too, and can be set per user or per profile.

Never comment out the trigger in production, and never ship a deployment just to turn logic off. Mention the risk as well: a bypass skips your logic, so the migration plan must cover what the trigger would have done, such as recalculating rollups after the load.

How do you test a trigger properly?

Test the behaviour, in bulk, including failures:

@isTest
static void countsContactsAcrossChunks() {
    Account acc = new Account(Name = 'Acme');
    insert acc;
    List<Contact> contacts = new List<Contact>();
    for (Integer i = 0; i < 250; i++) {
        contacts.add(new Contact(LastName = 'Test ' + i, AccountId = acc.Id));
    }

    Test.startTest();
    insert contacts;
    Test.stopTest();

    Account refreshed = [SELECT Contact_Count__c FROM Account WHERE Id = :acc.Id];
    Assert.areEqual(250, refreshed.Contact_Count__c.intValue(), 'Every contact should be counted');
}

250 records crosses a chunk boundary, which is what catches a broken static flag. Add tests for update with reparenting, delete and undelete. For validation, use Database.insert(records, false) and assert on the errors in the SaveResults. Use @testSetup for shared data and the Assert class for clear messages.

A deployment to production needs 75% coverage across your Apex and some coverage for every trigger, but coverage isn't the goal. Assertions are.

When do you choose a trigger over a record-triggered flow?

Salesforce's guidance is to start with Flow for automation, and for simple same-record updates a before-save flow is the right tool: it's fast and admins can maintain it.

I'd choose a trigger when:

  • the logic is complex, with lots of branching, collections or calculations that are painful to maintain in a flow,
  • the volume is high and you need tight control over queries, DML and CPU,
  • you need precise error handling, partial success or careful ordering with other logic,
  • the logic needs to be reused by Apex services, batch jobs and integrations.

Whatever you choose, pick one primary tool per object, or at least one owner per field. Most trigger and flow bugs in interviews, and in production, come from both tools changing the same thing.

What the interviewer is really checking: judgement, not loyalty to one tool.

How do you start async work from a trigger without hitting limits?

Enqueue once per trigger invocation, not once per record, and remember that one large DML runs your trigger several times in the same transaction. A synchronous transaction can enqueue up to 50 Queueable jobs, and a single Apex DML on thousands of records can fire the trigger dozens of times.

Practical rules:

  • Collect Ids for the whole chunk and enqueue a single job.
  • Check Limits.getQueueableJobs() against Limits.getLimitQueueableJobs() before enqueuing, and fall back to a different design, such as a platform event or a scheduled batch that picks up flagged records, when you're near the limit.
  • Know your context. If the trigger fires from a batch job or a future method, some async options aren't available. For example, a future method can't call another future method, and a Queueable can enqueue only one child job. System.isBatch(), System.isFuture() and System.isQueueable() tell you where you are.

For high-volume integrations, a platform event or a "needs sync" flag processed by a scheduled batch is usually more robust than enqueuing from the trigger.

How to practise these trigger questions out loud

Trigger questions are easy to read and hard to answer under pressure, because the follow-ups are about edge cases. Here's how I'd practise:

  1. Answer before you read. Say your answer, then compare. Mark where you forgot delete, undelete or reparenting.
  2. Write the contact-count trigger from a blank page until you can do it in ten minutes, covering all four events.
  3. Break it on purpose. Add a static Boolean guard, insert 250 records in a test and watch the count go wrong. Then fix it with an old and new comparison.
  4. Draw the order of execution from memory once a day for a week. Interviewers love "where does this flow run relative to my trigger?"
  5. Explain your trade-offs out loud: why before and not after, why a Queueable and not a future method, why without sharing for a rollup.

The free Apex Triggers Framework 101 course builds a handler framework live, from basic triggers up, and the Interview Questions course has a section on trigger questions. The Lifetime plan also includes the 700+ page Interview Questions Book.

Want 700+ pages of this?

The Salesforce Interview Questions & Answers Book goes far deeper than this page — it's included with the Lifetime plan, alongside the Real-Time Projects series.

Frequently asked questions

Are trigger questions still asked now that Flow is recommended?

Yes. Most orgs with developers still have triggers, and complex logic, high volumes and strict ordering are still trigger territory. Expect questions on when you'd choose a trigger over a record-triggered flow.

Will I be asked to write a trigger in the interview?

Very often. A typical task is a bulkified trigger that updates a related record or a rollup count, written in a shared editor. Interviewers watch for queries in loops, missing null checks and whether you handle update and delete, not just insert.

Should I mention a specific trigger framework?

Mention the pattern rather than a brand: one trigger per object, a handler class, logic in services, and a bypass switch. If you've used a known framework, say which and why, but be ready to explain it without it.

What's the most common mistake candidates make with trigger questions?

Answering for a single record. Every trigger answer should hold for 200 records at once, for updates that change a lookup, and for deletes and undeletes.

Keep going

Keep me in the loop!

Email me when a new episode is published (promise, I will not spam you)

You're signing up to receive emails from Salesforce Casts