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
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?
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;
}
}
}
}
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
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?
// 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;
}
}
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)
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.
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();
}
}
- One Trigger Per Object: Always route trigger events through a centralized Handler class using a framework (like
fflibor simple handler pattern). - Zero SOQL/DML in Loops: Never place SOQL queries or DML statements inside
forloops. Always collect IDs in sets and perform batch operations. - Handle Reparenting: When updating records, check both
Trigger.newandTrigger.oldMapto recalculate parents if a lookup relationship changes. - Security Enforcement: Always include
WITH USER_MODEin SOQL queries or useSecurity.stripInaccessibleto enforce sharing and field permissions.