Skip to main content

Salesforce Apex Trigger Interview Questions: 3 Real-World Scenarios with Solutions

In plain words: Scenario-based Apex trigger interview questions assess whether a developer understands the Salesforce Order of Execution, how to write bulkified code that never exceeds governor limits, and when to use before vs. after events properly.

During technical interviews for Salesforce Developer and Technical Architect roles, interviewers look beyond basic syntax. They present real-world scenarios to evaluate your understanding of bulkification, recursion management, governor limits, and clean trigger architecture. Below are three classic trigger scenarios with production-ready solutions.

Scenario 1: Using Before Update for Direct Field Updates

Question:

Whenever a Case status changes to "Closed", automatically set a custom date/time field (Closed_Timestamp__c) to the current timestamp. If reopened, clear the field. How do you implement this efficiently without DML statements?

Solution: Use a before update trigger to update field values in memory directly on Trigger.new without issuing an explicit update DML statement.
trigger CaseTrigger on Case (before update) {
    for (Case currentCase : Trigger.new) {
        Case oldCase = Trigger.oldMap.get(currentCase.Id);

        // Check if Status field has changed
        if (currentCase.Status != oldCase.Status) {
            if (currentCase.Status == 'Closed') {
                currentCase.Closed_Timestamp__c = System.now();
            } else if (oldCase.Status == 'Closed') {
                currentCase.Closed_Timestamp__c = null;
            }
        }
    }
}
Warning Trap: Never execute update Trigger.new; inside a before trigger. Modifying records in Trigger.new during a before event automatically commits those changes to the database. Running DML on Trigger.new inside a before trigger causes a fatal System.FinalException: Record is read-only or triggers infinite recursion.

Scenario 2: Preventing Trigger Recursion

Question:

An after update trigger updates a related record, which in turn causes another update back to the original record, creating an infinite recursive loop. How do you prevent this loop in Apex?

Solution: Maintain a static set of processed record IDs or a static boolean flag in an Apex handler/utility class. Static variables remain in memory throughout the entire transaction execution context.
// 1. Static Recursion Handler Class
public class AccountTriggerHandler {
    // Static set persists across recursive trigger calls in the same transaction
    public static Set<Id> processedRecordIds = new Set<Id>();
}

// 2. Trigger Implementation
trigger AccountTrigger on Account (after update) {
    List<Contact> contactsToUpdate = new List<Contact>();

    for (Account acc : Trigger.new) {
        // Process record only if it has not been processed in this transaction
        if (!AccountTriggerHandler.processedRecordIds.contains(acc.Id)) {
            AccountTriggerHandler.processedRecordIds.add(acc.Id);

            // Add business logic (e.g., syncing address to related contacts)
            // contactsToUpdate.add(new Contact(AccountId = acc.Id, ...));
        }
    }

    if (!contactsToUpdate.isEmpty()) {
        update contactsToUpdate;
    }
}
Warning Trap: Avoid using a single static Boolean isFirstRun = true; flag if your org processes batch updates. In batch executions, a single boolean flag set to false after chunk 1 will prevent all subsequent 200-record chunks from processing. Using a Set<Id> of processed records is much safer for bulk operations.

Scenario 3: Bulkified Rollup Summary (Aggregate SOQL)

Question:

Write an Apex trigger on Opportunity that calculates and stores the average amount of all related opportunities on the parent Account record (Avg_Opportunity_Amount__c). The solution must handle bulk inserts, updates, deletes, and undeletes without hitting governor limits.

Solution: Collect all unique parent AccountId references across insert, update, delete, and undelete events, then run a single bulkified AggregateResult query to update the parent accounts.
trigger OpportunityRollupTrigger on Opportunity (after insert, after update, after delete, after undelete) {
    Set<Id> accountIds = new Set<Id>();

    // Collect parent Account IDs from new records (insert, update, undelete)
    if (Trigger.isInsert || Trigger.isUpdate || Trigger.isUndelete) {
        for (Opportunity opp : Trigger.new) {
            if (opp.AccountId != null) {
                accountIds.add(opp.AccountId);
            }
            // If parent account was changed, recalculate old parent too
            if (Trigger.isUpdate) {
                Opportunity oldOpp = Trigger.oldMap.get(opp.Id);
                if (oldOpp.AccountId != null && oldOpp.AccountId != opp.AccountId) {
                    accountIds.add(oldOpp.AccountId);
                }
            }
        }
    }

    // Collect parent Account IDs from deleted records
    if (Trigger.isDelete) {
        for (Opportunity opp : Trigger.old) {
            if (opp.AccountId != null) {
                accountIds.add(opp.AccountId);
            }
        }
    }

    if (accountIds.isEmpty()) {
        return;
    }

    // Map to hold accounts ready for update
    Map<Id, Account> accountsToUpdateMap = new Map<Id, Account>();

    // Default all affected accounts to 0/null in case all child opps were removed
    for (Id accId : accountIds) {
        accountsToUpdateMap.put(accId, new Account(Id = accId, Avg_Opportunity_Amount__c = 0));
    }

    // Run bulkified aggregate query across all affected accounts
    for (AggregateResult result : [
        SELECT AccountId accId, AVG(Amount) avgAmt 
        FROM Opportunity 
        WHERE AccountId IN :accountIds AND Amount != NULL
        WITH USER_MODE
        GROUP BY AccountId
    ]) {
        Id accId = (Id) result.get('accId');
        Decimal avgAmount = (Decimal) result.get('avgAmt');
        accountsToUpdateMap.put(accId, new Account(Id = accId, Avg_Opportunity_Amount__c = avgAmount));
    }

    // Perform single bulk DML update
    if (!accountsToUpdateMap.isEmpty()) {
        update accountsToUpdateMap.values();
    }
}
360 Architecture Summary:
  • One Trigger Per Object: Always route trigger events through a centralized Handler class using a framework (like fflib or simple handler pattern).
  • Zero SOQL/DML in Loops: Never place SOQL queries or DML statements inside for loops. Always collect IDs in sets and perform batch operations.
  • Handle Reparenting: When updating records, check both Trigger.new and Trigger.oldMap to recalculate parents if a lookup relationship changes.
  • Security Enforcement: Always include WITH USER_MODE in SOQL queries or use Security.stripInaccessible to enforce sharing and field permissions.
Core Takeaway: Demonstrating clean trigger bulkification, understanding before vs. after execution contexts, and using static collections for recursion control are key indicators of a seasoned Salesforce Developer.