Salesforce Apex Triggers Interview Questions — 34 Real Scenarios with Code & Answers (2025)
Salesforce Apex Triggers — Complete Interview Questions Guide
When a new Contact is inserted: (1) Find the Email Template by DeveloperName, (2) Loop through contacts skipping null emails, (3) Build SingleEmailMessage with setTemplateId() and setTargetObjectId(), (4) Send all in one call.
trigger ContactWelcomeTrigger on Contact (after insert) { ContactWelcomeHelper.sendWelcomeEmail(Trigger.new); } public class ContactWelcomeHelper { public static void sendWelcomeEmail(List<Contact> newContacts) { EmailTemplate template = [ SELECT Id FROM EmailTemplate WHERE DeveloperName = 'Contact_Welcome_Template' LIMIT 1 ]; List<Messaging.SingleEmailMessage> emailList = new List<Messaging.SingleEmailMessage>(); for (Contact con : newContacts) { if (con.Email == null) continue; Messaging.SingleEmailMessage mail = new Messaging.SingleEmailMessage(); mail.setTemplateId(template.Id); mail.setTargetObjectId(con.Id); mail.setSaveAsActivity(false); emailList.add(mail); } if (!emailList.isEmpty()) Messaging.sendEmail(emailList); } }
after insert and not before insert?before insert, the Contact has no Id yet. setTargetObjectId(con.Id) needs the Id which only exists after the record is saved.DeveloperName and not Id?DeveloperName stays consistent across all environments.sendEmail() call. No SOQL/DML inside loop."I wrote an after insert trigger on Contact that queries the Email Template by DeveloperName, builds SingleEmailMessage objects using setTemplateId and setTargetObjectId, and sends all in one Messaging.sendEmail() call — fully bulkified."
Use before insert, before update to block before save. Collect all incoming Emails & Phones into Sets, fire one SOQL using OR, then use field-level addError() to block the specific duplicate field.
trigger ContactDuplicateTrigger on Contact (before insert, before update) { ContactDuplicateHelper.checkDuplicates(Trigger.new, Trigger.oldMap); } public class ContactDuplicateHelper { public static void checkDuplicates(List<Contact> newContacts, Map<Id, Contact> oldMap) { Set<String> emails = new Set<String>(); Set<String> phones = new Set<String>(); for (Contact con : newContacts) { if (oldMap != null) { Contact old = oldMap.get(con.Id); if (con.Email == old.Email && con.Phone == old.Phone) continue; } if (con.Email != null) emails.add(con.Email); if (con.Phone != null) phones.add(con.Phone); } if (emails.isEmpty() && phones.isEmpty()) return; List<Contact> existing = [SELECT Id, Email, Phone FROM Contact WHERE Email IN :emails OR Phone IN :phones]; Set<String> exEmails = new Set<String>(); Set<String> exPhones = new Set<String>(); for (Contact c : existing) { if (c.Email != null) exEmails.add(c.Email); if (c.Phone != null) exPhones.add(c.Phone); } for (Contact con : newContacts) { if (con.Email != null && exEmails.contains(con.Email)) con.Email.addError('Duplicate found! This Email already exists.'); if (con.Phone != null && exPhones.contains(con.Phone)) con.Phone.addError('Duplicate found! This Phone already exists.'); } } }
before insert, before update?addError() in before context stops DML completely. In after context — record is already saved, too late.addError() instead of record-level?con.Email.addError() highlights the specific field causing the issue — much better UX than a generic record-level error.oldMap check skips the Contact if Email and Phone haven't changed. So a contact never flags itself."I wrote a before insert, before update trigger that collects Emails and Phones into Sets, fires a single SOQL using OR, and calls field-level addError() to block duplicates. For updates, oldMap comparison ensures a contact never flags itself. Fully bulkified."
trigger TaskDeleteTrigger on Task (before delete) { TaskDeleteHelper.preventUnauthorizedDelete(Trigger.old); } public class TaskDeleteHelper { public static void preventUnauthorizedDelete(List<Task> oldTasks) { String profileName = [ SELECT Name FROM Profile WHERE Id = :UserInfo.getProfileId() LIMIT 1 ].Name; if (!profileName.trim().equalsIgnoreCase('System Administrator')) { for (Task t : oldTasks) t.addError('Only System Administrators can delete Tasks.'); } } }
Trigger.old and not Trigger.new?Trigger.new is always null. Trigger.old holds the records about to be deleted.FeatureManagement.checkPermission('Can_Delete_Task') with a Custom Permission assigned to System Admin. More flexible — no code change needed if another profile needs delete access."I wrote a before delete trigger on Task that queries the running user's Profile Name using UserInfo.getProfileId() once outside the loop, and if not System Administrator, calls addError() on every task in Trigger.old. As an alternative, I'd prefer Custom Permission for better flexibility."
trigger AccountAddressTrigger on Account (before insert) { AccountAddressHelper.copyBillingToShipping(Trigger.new); } public class AccountAddressHelper { public static void copyBillingToShipping(List<Account> newAccounts) { for (Account acc : newAccounts) { if (acc.BillingStreet != null || acc.BillingCity != null) { acc.ShippingStreet = acc.BillingStreet; acc.ShippingCity = acc.BillingCity; acc.ShippingState = acc.BillingState; acc.ShippingCountry = acc.BillingCountry; acc.ShippingPostalCode = acc.BillingPostalCode; } } } }
before insert? Why no DML needed?"I wrote a before insert trigger on Account that loops through Trigger.new and directly assigns all 5 Billing fields to Shipping. Since it's a before trigger, changes are saved automatically — zero SOQL, zero DML, most efficient approach possible."
Must be after insert because OLI needs OpportunityId. Get Standard Pricebook Id, query one active PricebookEntry, build OLI with mandatory fields: OpportunityId, PricebookEntryId, Quantity, UnitPrice.
trigger OpportunityLineItemTrigger on Opportunity (after insert) { OpportunityLineItemHelper.insertDefaultLineItem(Trigger.new); } public class OpportunityLineItemHelper { public static void insertDefaultLineItem(List<Opportunity> newOpps) { Id stdPBId = Test.isRunningTest() ? Test.getStandardPricebookId() : [SELECT Id FROM Pricebook2 WHERE IsStandard = true LIMIT 1].Id; List<PricebookEntry> pbeList = [ SELECT Id, UnitPrice, Product2Id FROM PricebookEntry WHERE Pricebook2Id = :stdPBId AND IsActive = true LIMIT 1]; if (pbeList.isEmpty()) return; PricebookEntry pbe = pbeList[0]; List<OpportunityLineItem> oliList = new List<OpportunityLineItem>(); for (Opportunity opp : newOpps) { OpportunityLineItem oli = new OpportunityLineItem(); oli.OpportunityId = opp.Id; oli.PricebookEntryId = pbe.Id; oli.Quantity = 1; oli.UnitPrice = pbe.UnitPrice; oliList.add(oli); } if (!oliList.isEmpty()) insert oliList; } }
after insert?Test.isRunningTest() for Pricebook?Test.getStandardPricebookId() is the official way to get it in tests. Without this guard, test classes will fail."I wrote an after insert trigger on Opportunity that queries an active PricebookEntry from the Standard Pricebook, builds OLI records with OpportunityId, PricebookEntryId, Quantity=1, and UnitPrice, and inserts all in one DML. Test.isRunningTest() handles the Standard Pricebook limitation in test context."
OLI → Opportunity → Account. OLI has no direct AccountId — must query Opportunity first. Mandatory Asset fields: Name, AccountId, Product2Id, Status.
trigger OLIAssetTrigger on OpportunityLineItem (after insert) { OLIAssetHelper.createAssets(Trigger.new); } public class OLIAssetHelper { public static void createAssets(List<OpportunityLineItem> newOLIs) { Set<Id> oppIds = new Set<Id>(); for (OpportunityLineItem oli : newOLIs) oppIds.add(oli.OpportunityId); Map<Id, Opportunity> oppMap = new Map<Id, Opportunity>([ SELECT Id, AccountId FROM Opportunity WHERE Id IN :oppIds AND AccountId != null]); List<Asset> assetList = new List<Asset>(); for (OpportunityLineItem oli : newOLIs) { Opportunity opp = oppMap.get(oli.OpportunityId); if (opp == null) continue; Asset ast = new Asset(); ast.Name = oli.Name; ast.Product2Id = oli.Product2Id; ast.AccountId = opp.AccountId; ast.Quantity = oli.Quantity != null ? oli.Quantity : 1; ast.Price = oli.UnitPrice != null ? oli.UnitPrice : 0; ast.PurchaseDate = Date.today(); ast.Status = 'Purchased'; assetList.add(ast); } if (!assetList.isEmpty()) insert assetList; } }
"I wrote an after insert trigger on OLI that collects Opportunity Ids, queries them into a Map with AccountId, then builds Asset records using Name, AccountId from Opportunity, Product2Id, Quantity, UnitPrice, and Status=Purchased. All inserted in one DML. 1 SOQL, 1 DML, fully bulkified."
When we insert the duplicate Lead, the same trigger fires again → infinite loop → governor limit hit. Must use a static boolean recursion guard.
trigger LeadDuplicateTrigger on Lead (after insert) { LeadDuplicateHelper.createDuplicateLeads(Trigger.new); } public class LeadDuplicateHelper { private static Boolean isExecuting = false; // Recursion guard public static void createDuplicateLeads(List<Lead> newLeads) { if (isExecuting) return; // Already running — exit isExecuting = true; List<Lead> duplicates = new List<Lead>(); for (Lead original : newLeads) { Lead dup = original.clone(false, true, false, false); dup.LastName = original.LastName + ' (Duplicate)'; duplicates.add(dup); } if (!duplicates.isEmpty()) insert duplicates; isExecuting = false; } }
after insert?isExecuting persists within the same transaction — second trigger invocation returns immediately.clone(false, true, false, false) do?"I wrote an after insert trigger on Lead that uses clone(false,true,false,false) to copy all fields, appends (Duplicate) to LastName, and inserts all duplicates in one DML. The most critical part is the static boolean recursion guard — without it, inserting the duplicate fires the trigger again causing an infinite loop."
trigger AccountOpportunityTrigger on Account (after update) { AccountOpportunityHelper.closeOldOpportunities(Trigger.new); } public class AccountOpportunityHelper { public static void closeOldOpportunities(List<Account> updatedAccounts) { Set<Id> accountIds = new Set<Id>(); for (Account acc : updatedAccounts) accountIds.add(acc.Id); Date thirtyDaysAgo = Date.today().addDays(-30); List<Opportunity> oppsToUpdate = [ SELECT Id, StageName FROM Opportunity WHERE AccountId IN :accountIds AND CreatedDate < :thirtyDaysAgo AND StageName != 'Closed Won']; if (oppsToUpdate.isEmpty()) return; for (Opportunity opp : oppsToUpdate) opp.StageName = 'Closed Lost'; update oppsToUpdate; } }
CreatedDate < thirtyDaysAgo and not >?"I wrote an after update trigger on Account that collects Account Ids, calculates Date.today().addDays(-30), and queries Opportunities where CreatedDate < thirtyDaysAgo and StageName != Closed Won. Then sets StageName=Closed Lost and updates in one DML. 1 SOQL, 1 DML, fully bulkified."
DML 1: Insert Contact. DML 2: Update Account with the new Contact Id. After inserting Contacts, Salesforce populates their Ids on the same object reference in memory — no re-query needed.
trigger AccountClientContactTrigger on Account (after insert) { AccountClientContactHelper.createClientContact(Trigger.new); } public class AccountClientContactHelper { public static void createClientContact(List<Account> newAccounts) { List<Contact> contactsToInsert = new List<Contact>(); for (Account acc : newAccounts) { Contact con = new Contact(); con.LastName = String.isNotBlank(acc.Name) ? acc.Name : 'Unknown'; con.AccountId = acc.Id; contactsToInsert.add(con); } if (contactsToInsert.isEmpty()) return; insert contactsToInsert; // DML 1 — Contact Ids now populated // Map Contact back to Account using con.AccountId Map<Id, Id> accountToContactMap = new Map<Id, Id>(); for (Contact con : contactsToInsert) accountToContactMap.put(con.AccountId, con.Id); List<Account> accountsToUpdate = new List<Account>(); for (Account acc : newAccounts) { if (accountToContactMap.containsKey(acc.Id)) { Account a = new Account(); a.Id = acc.Id; a.Client_Contact__c = accountToContactMap.get(acc.Id); accountsToUpdate.add(a); } } if (!accountsToUpdate.isEmpty()) update accountsToUpdate; // DML 2 } }
"I wrote an after insert trigger on Account that creates a Contact per Account using Account Name as LastName, inserts all Contacts (DML 1 — Ids now populated), maps Contact.AccountId to Contact.Id, then builds partial Account update objects and updates Client_Contact__c in DML 2. Zero SOQL, 2 DML, fully bulkified."
OLI → Opportunity → Account → Client_Contact__r (Contact → Email). Also need Product details via PricebookEntry.Product2. Trigger.new doesn't carry relationship fields — must re-query OLIs.
trigger OLIEmailTrigger on OpportunityLineItem (after insert) { OLIEmailHelper.sendClientContactEmail(Trigger.new); } public class OLIEmailHelper { public static void sendClientContactEmail(List<OpportunityLineItem> newOLIs) { Set<Id> oppIds = new Set<Id>(); for (OpportunityLineItem oli : newOLIs) oppIds.add(oli.OpportunityId); // Query Opportunity → Account → Client Contact Map<Id, Opportunity> oppMap = new Map<Id, Opportunity>([ SELECT Id, Account.Name, Account.Client_Contact__c, Account.Client_Contact__r.FirstName, Account.Client_Contact__r.LastName, Account.Client_Contact__r.Email FROM Opportunity WHERE Id IN :oppIds AND Account.Client_Contact__c != null AND Account.Client_Contact__r.Email != null]); // Re-query OLIs with Product details List<OpportunityLineItem> oliDetails = [ SELECT Id, OpportunityId, PricebookEntry.Product2.Name, PricebookEntry.Product2.ProductCode, UnitPrice, ListPrice, Discount FROM OpportunityLineItem WHERE Id IN :new Map<Id, OpportunityLineItem>(newOLIs).keySet()]; String userName = UserInfo.getFirstName() + ' ' + UserInfo.getLastName(); List<Messaging.SingleEmailMessage> emailList = new List<Messaging.SingleEmailMessage>(); for (OpportunityLineItem oli : oliDetails) { Opportunity opp = oppMap.get(oli.OpportunityId); if (opp == null) continue; String email = opp.Account.Client_Contact__r.Email; String name = opp.Account.Client_Contact__r.FirstName + ' ' + opp.Account.Client_Contact__r.LastName; String body = '<p>Hi <b>' + name + '</b>,</p>' + '<p>Your Order has been processed.</p>' + '<p>Product: ' + oli.PricebookEntry?.Product2?.Name + '<br/>' + 'Code: ' + oli.PricebookEntry?.Product2?.ProductCode + '<br/>' + 'Unit Price: ' + oli.UnitPrice + '<br/>' + 'List Price: ' + oli.ListPrice + '<br/>' + 'Discount: ' + oli.Discount + '%</p>' + '<p>Thanks,<br/><b>' + userName + '</b></p>'; Messaging.SingleEmailMessage mail = new Messaging.SingleEmailMessage(); mail.setToAddresses(new List<String>{email}); mail.setSubject('Your Order Has Been Processed'); mail.setHtmlBody(body); mail.setSaveAsActivity(false); emailList.add(mail); } if (!emailList.isEmpty()) Messaging.sendEmail(emailList); } }
?. safe navigation operator?oli.PricebookEntry?.Product2?.Name returns null safely instead of throwing a NullPointerException — clean null safety pattern."I wrote an after insert trigger on OLI that queries Opportunities with Account's Client_Contact__r in one SOQL, re-queries OLIs with Product details since Trigger.new doesn't carry relationship fields, builds HTML emails with product info and running user name from UserInfo, and sends all in one sendEmail() call. 2 SOQL, 0 DML, fully bulkified."
trigger AccountAdminEmailTrigger on Account (after insert) { AccountAdminEmailHelper.sendEmailToAdmin(Trigger.new); } public class AccountAdminEmailHelper { public static void sendEmailToAdmin(List<Account> newAccounts) { List<User> adminUsers = [ SELECT Id, Email FROM User WHERE Profile.Name = 'System Administrator' AND IsActive = true LIMIT 1]; if (adminUsers.isEmpty()) return; String adminEmail = adminUsers[0].Email; List<Messaging.SingleEmailMessage> emailList = new List<Messaging.SingleEmailMessage>(); for (Account acc : newAccounts) { String accName = String.isNotBlank(acc.Name) ? acc.Name : 'Unknown Account'; Messaging.SingleEmailMessage mail = new Messaging.SingleEmailMessage(); mail.setToAddresses(new List<String>{adminEmail}); mail.setSubject('New Account Created — ' + accName); mail.setHtmlBody('<p>Hi Admin,</p><p>An account has been created and name is <b>' + accName + '</b>.</p>'); mail.setSaveAsActivity(false); emailList.add(mail); } if (!emailList.isEmpty()) Messaging.sendEmail(emailList); } }
Label.Admin_Notification_Email — no hardcoding, no governor limit usage."I wrote an after insert trigger on Account that queries the active System Administrator's email once outside the loop, builds a SingleEmailMessage per Account with subject and HTML body containing the Account Name, and sends all in one sendEmail() call. Production enhancement: store admin email in a Custom Label to eliminate SOQL entirely."
Don't just add new quantity — query ALL existing OLIs for that Account and sum from scratch. This is idempotent and handles retries/deletes correctly.
trigger OLIQuantityTrigger on OpportunityLineItem (after insert, after update, after delete) { List<OpportunityLineItem> oliList = Trigger.isDelete ? Trigger.old : Trigger.new; OLIQuantityHelper.updateAccountTotalQuantities(oliList); } public class OLIQuantityHelper { public static void updateAccountTotalQuantities(List<OpportunityLineItem> oliList) { Set<Id> oppIds = new Set<Id>(); for (OpportunityLineItem oli : oliList) oppIds.add(oli.OpportunityId); Map<Id, Opportunity> oppMap = new Map<Id, Opportunity>([ SELECT Id, AccountId FROM Opportunity WHERE Id IN :oppIds AND AccountId != null]); Set<Id> accountIds = new Set<Id>(); for (Opportunity opp : oppMap.values()) accountIds.add(opp.AccountId); if (accountIds.isEmpty()) return; // SUM all OLIs via AggregateResult — clean and performant Map<Id, Decimal> accountQtyMap = new Map<Id, Decimal>(); for (Id id : accountIds) accountQtyMap.put(id, 0); for (AggregateResult ar : [ SELECT Opportunity.AccountId accId, SUM(Quantity) total FROM OpportunityLineItem WHERE Opportunity.AccountId IN :accountIds AND Quantity != null GROUP BY Opportunity.AccountId]) { accountQtyMap.put((Id)ar.get('accId'), (Decimal)ar.get('total')); } List<Account> toUpdate = new List<Account>(); for (Account acc : [SELECT Id, Total_Sales_Quantities__c FROM Account WHERE Id IN :accountIds]) { Decimal newTotal = accountQtyMap.get(acc.Id); if ((acc.Total_Sales_Quantities__c != null ? acc.Total_Sales_Quantities__c : 0) != newTotal) { Account a = new Account(); a.Id = acc.Id; a.Total_Sales_Quantities__c = newTotal; toUpdate.add(a); } } if (!toUpdate.isEmpty()) update toUpdate; } }
"I wrote an after insert/update/delete trigger on OLI using AggregateResult SUM(Quantity) GROUP BY AccountId — one SOQL gives accurate total per Account. I initialize all Accounts with 0 so deleted-OLI accounts get reset correctly, query current values, and only update changed Accounts. Full recalculation, idempotent, 3 SOQL, 1 DML."
trigger LeadWelcomeEmailTrigger on Lead (after insert) { LeadWelcomeEmailHelper.sendWelcomeEmail(Trigger.new); } public class LeadWelcomeEmailHelper { public static void sendWelcomeEmail(List<Lead> newLeads) { List<Document> docs = [ SELECT Id, Name, Body, ContentType FROM Document WHERE DeveloperName = 'Welcome_Document' AND ContentType = 'application/pdf' LIMIT 1]; if (docs.isEmpty()) return; Messaging.EmailFileAttachment att = new Messaging.EmailFileAttachment(); att.setFileName(docs[0].Name + '.pdf'); att.setBody(docs[0].Body); att.setContentType(docs[0].ContentType); List<Messaging.SingleEmailMessage> emailList = new List<Messaging.SingleEmailMessage>(); for (Lead lead : newLeads) { if (lead.Email == null) continue; Messaging.SingleEmailMessage mail = new Messaging.SingleEmailMessage(); mail.setToAddresses(new List<String>{lead.Email}); mail.setSubject('Welcome'); mail.setHtmlBody('<p>Please find the attached PDF.</p>'); mail.setFileAttachments(new List<Messaging.EmailFileAttachment>{att}); mail.setSaveAsActivity(false); emailList.add(mail); } if (!emailList.isEmpty()) Messaging.sendEmail(emailList); } }
VersionData instead of Body — everything else is the same."I wrote an after insert trigger on Lead that queries the PDF from Documents by DeveloperName (not Id — works across environments), builds one EmailFileAttachment with setBody(Blob) outside the loop, and reuses it for all emails. Subject: Welcome, Body: Please find the attached PDF. 1 SOQL, 0 DML, fully bulkified."
trigger AccountTypeChangeTrigger on Account (after update) { AccountTypeChangeHelper.sendEmailOnTypeChange(Trigger.new, Trigger.oldMap); } public class AccountTypeChangeHelper { public static void sendEmailOnTypeChange(List<Account> updatedAccounts, Map<Id, Account> oldMap) { Map<Id, String> accountNameMap = new Map<Id, String>(); for (Account acc : updatedAccounts) { if (acc.Type != oldMap.get(acc.Id).Type) accountNameMap.put(acc.Id, acc.Name); } if (accountNameMap.isEmpty()) return; List<Contact> contacts = [ SELECT Id, FirstName, LastName, Email, AccountId FROM Contact WHERE AccountId IN :accountNameMap.keySet() AND Email != null]; if (contacts.isEmpty()) return; List<Messaging.SingleEmailMessage> emailList = new List<Messaging.SingleEmailMessage>(); for (Contact con : contacts) { String accName = accountNameMap.get(con.AccountId); String body = '<p>Hi <b>' + con.FirstName + '</b>,</p>' + '<p>Your account information has been updated successfully.</p>' + '<p>Account Name : <b>' + accName + '</b></p>'; Messaging.SingleEmailMessage mail = new Messaging.SingleEmailMessage(); mail.setToAddresses(new List<String>{con.Email}); mail.setSubject('Account Update Info'); mail.setHtmlBody(body); mail.setSaveAsActivity(false); emailList.add(mail); } if (!emailList.isEmpty()) Messaging.sendEmail(emailList); } }
acc.Type != oldMap.get(acc.Id).Type?"I wrote an after update trigger on Account that uses oldMap to detect Type change, stores Account Names in a Map, queries all Contacts with non-null email in one SOQL, and builds emails with Subject: Account Update Info and body including Account Name. All sent in one sendEmail() call. 1 SOQL, 0 DML, fully bulkified."
trigger OLIDeleteOpportunityTrigger on OpportunityLineItem (before delete) { OLIDeleteOpportunityHelper.deleteParentOpportunity(Trigger.old); } public class OLIDeleteOpportunityHelper { public static void deleteParentOpportunity(List<OpportunityLineItem> deletedOLIs) { Set<Id> oppIds = new Set<Id>(); for (OpportunityLineItem oli : deletedOLIs) if (oli.OpportunityId != null) oppIds.add(oli.OpportunityId); if (oppIds.isEmpty()) return; List<Opportunity> oppsToDelete = [ SELECT Id FROM Opportunity WHERE Id IN :oppIds]; if (!oppsToDelete.isEmpty()) { try { delete oppsToDelete; } catch (DmlException e) { System.debug('Delete failed: ' + e.getMessage()); } } } }
before delete preferred?"I wrote a before delete trigger on OLI that collects OpportunityIds from Trigger.old — since Trigger.new is null in delete context — queries those Opportunities and deletes them in one DML. Key design consideration: whether to delete only when no remaining OLIs exist. 1 SOQL, 1 DML, fully bulkified."
trigger OpportunityStageChangeTrigger on Opportunity (after update) { OpportunityStageChangeHelper.sendEmailOnStageChange(Trigger.new, Trigger.oldMap); } public class OpportunityStageChangeHelper { public static void sendEmailOnStageChange(List<Opportunity> updatedOpps, Map<Id, Opportunity> oldMap) { Set<Id> accountIds = new Set<Id>(); Map<Id, String> oppAccountMap = new Map<Id, String>(); for (Opportunity opp : updatedOpps) { if (opp.StageName != oldMap.get(opp.Id).StageName && opp.AccountId != null) { accountIds.add(opp.AccountId); oppAccountMap.put(opp.AccountId, opp.Name); } } if (accountIds.isEmpty()) return; List<Account> accounts = [ SELECT Id, Name, Client_Contact__r.FirstName, Client_Contact__r.LastName, Client_Contact__r.Email FROM Account WHERE Id IN :accountIds AND Client_Contact__c != null AND Client_Contact__r.Email != null]; if (accounts.isEmpty()) return; List<Messaging.SingleEmailMessage> emailList = new List<Messaging.SingleEmailMessage>(); for (Account acc : accounts) { String body = '<p>Hi <b>' + acc.Client_Contact__r.FirstName + '</b>,</p>' + '<p>Your account information has been updated successfully.</p>' + '<p>Account Name : <b>' + acc.Name + '</b></p>' + '<p>Opportunity : <b>' + oppAccountMap.get(acc.Id) + '</b></p>'; Messaging.SingleEmailMessage mail = new Messaging.SingleEmailMessage(); mail.setToAddresses(new List<String>{acc.Client_Contact__r.Email}); mail.setSubject('Account Update Info'); mail.setHtmlBody(body); mail.setSaveAsActivity(false); emailList.add(mail); } if (!emailList.isEmpty()) Messaging.sendEmail(emailList); } }
opp.StageName != oldMap.get(opp.Id).StageName?"I wrote an after update trigger on Opportunity that uses oldMap to detect StageName changes, stores Opportunity Name in a Map, queries Accounts with Client_Contact__r in one SOQL, builds emails with Subject: Account Update Info, and sends all in one sendEmail(). 1 SOQL, 0 DML, fully bulkified."
// Quote mandatory fields: Name, OpportunityId, Pricebook2Id, Status // Check existing Quotes first to prevent duplicates (idempotency) trigger OLICreateQuoteTrigger on OpportunityLineItem (after insert) { OLICreateQuoteHelper.createQuoteOnOLIInsert(Trigger.new); } public class OLICreateQuoteHelper { public static void createQuoteOnOLIInsert(List<OpportunityLineItem> newOLIs) { Set<Id> oppIds = new Set<Id>(); for (OpportunityLineItem oli : newOLIs) oppIds.add(oli.OpportunityId); Map<Id, Opportunity> oppMap = new Map<Id, Opportunity>([ SELECT Id, Name, Pricebook2Id, CloseDate, OwnerId FROM Opportunity WHERE Id IN :oppIds]); Set<Id> oppsWithQuotes = new Set<Id>(); for (Quote q : [SELECT OpportunityId FROM Quote WHERE OpportunityId IN :oppIds]) oppsWithQuotes.add(q.OpportunityId); List<Quote> quotesToInsert = new List<Quote>(); for (Opportunity opp : oppMap.values()) { if (oppsWithQuotes.contains(opp.Id)) continue; Quote q = new Quote(); q.Name = 'Quote - ' + opp.Name; q.OpportunityId = opp.Id; q.Status = 'Draft'; q.Pricebook2Id = opp.Pricebook2Id; q.ExpirationDate = opp.CloseDate; q.OwnerId = opp.OwnerId; quotesToInsert.add(q); } if (!quotesToInsert.isEmpty()) insert quotesToInsert; } }
"I wrote an after insert trigger on OLI that queries Opportunity details and existing Quotes in two SOQLs. For Opps without existing Quotes, I create Quote records with Name, Pricebook2Id matching the Opp, ExpirationDate=CloseDate, Status=Draft, and same OwnerId. Idempotent duplicate check prevents re-creation. 2 SOQL, 1 DML."
trigger AssetMinExpirationTrigger on Asset (after insert, after update, after delete, after undelete) { List<Asset> aList = Trigger.isDelete ? Trigger.old : Trigger.new; AssetMinExpirationHelper.updateMinExpirationDate(aList); } public class AssetMinExpirationHelper { public static void updateMinExpirationDate(List<Asset> assetList) { Set<Id> accountIds = new Set<Id>(); for (Asset ast : assetList) if (ast.AccountId != null) accountIds.add(ast.AccountId); if (accountIds.isEmpty()) return; Map<Id, Date> accountMinMap = new Map<Id, Date>(); for (Id id : accountIds) accountMinMap.put(id, null); for (AggregateResult ar : [ SELECT AccountId, MIN(UsageEndDate) minDate FROM Asset WHERE AccountId IN :accountIds AND UsageEndDate != null GROUP BY AccountId]) { accountMinMap.put((Id)ar.get('AccountId'), (Date)ar.get('minDate')); } List<Account> toUpdate = new List<Account>(); for (Account acc : [SELECT Id, Asset_Minimum_Expiration_Date__c FROM Account WHERE Id IN :accountIds]) { Date newMin = accountMinMap.get(acc.Id); if (acc.Asset_Minimum_Expiration_Date__c != newMin) { Account a = new Account(); a.Id = acc.Id; a.Asset_Minimum_Expiration_Date__c = newMin; toUpdate.add(a); } } if (!toUpdate.isEmpty()) update toUpdate; } }
"I wrote an after insert/update/delete/undelete trigger on Asset. The helper collects Account Ids, initializes all to null, fires one AggregateResult SOQL using MIN(UsageEndDate) GROUP BY AccountId, queries current Account values, and only fires DML when the min date changed. All Assets deleted = field cleared to null. 2 SOQL, 1 DML, fully bulkified."
Uses SUM(Quantity) GROUP BY Opportunity.AccountId AggregateResult. Initialize all Accounts with 0 before summing. Only update if value changed.
trigger OLITotalQuantityTrigger on OpportunityLineItem (after insert, after update, after delete, after undelete) { List<OpportunityLineItem> oliList = Trigger.isDelete ? Trigger.old : Trigger.new; OLITotalQuantityHelper.updateAccountTotalQuantity(oliList); } // Helper: same pattern as Q12 — SUM via AggregateResult, 3 SOQL, 1 DML // Key: Initialize all Account Ids with 0 before populating // Key: Only update if oldTotal != newTotal (prevent unnecessary DML)
"I wrote an after insert/update/delete/undelete trigger on OLI. Full recalculation using SUM(Quantity) AggregateResult. Initialize all Accounts with 0 so accounts with no remaining OLIs get reset. Only fire DML when value changed. 3 SOQL, 1 DML, fully bulkified, idempotent."
trigger ContactOpportunityAmountTrigger on Contact (after update) { ContactOpportunityAmountHelper.updateTotalOppAmount(Trigger.new, Trigger.oldMap); } public class ContactOpportunityAmountHelper { public static void updateTotalOppAmount(List<Contact> updatedContacts, Map<Id, Contact> oldMap) { Set<Id> accountIds = new Set<Id>(); for (Contact con : updatedContacts) { Contact oldCon = oldMap.get(con.Id); if (con.AccountId != null) accountIds.add(con.AccountId); // If AccountId changed — recalculate OLD Account too if (oldCon.AccountId != null && oldCon.AccountId != con.AccountId) accountIds.add(oldCon.AccountId); } if (accountIds.isEmpty()) return; Map<Id, Decimal> accountAmountMap = new Map<Id, Decimal>(); for (Id id : accountIds) accountAmountMap.put(id, 0); for (AggregateResult ar : [ SELECT AccountId, SUM(Amount) totalAmount FROM Opportunity WHERE AccountId IN :accountIds AND Amount != null GROUP BY AccountId]) { accountAmountMap.put((Id)ar.get('AccountId'), (Decimal)ar.get('totalAmount')); } List<Account> toUpdate = new List<Account>(); for (Account acc : [SELECT Id, Total_Opportunity_Amount__c FROM Account WHERE Id IN :accountIds]) { Decimal newTotal = accountAmountMap.get(acc.Id); Decimal oldTotal = acc.Total_Opportunity_Amount__c != null ? acc.Total_Opportunity_Amount__c : 0; if (oldTotal != newTotal) { Account a = new Account(); a.Id = acc.Id; a.Total_Opportunity_Amount__c = newTotal; toUpdate.add(a); } } if (!toUpdate.isEmpty()) update toUpdate; } }
"I wrote an after update trigger on Contact that collects both old and new AccountIds (to handle Contact account changes), fires one AggregateResult SUM(Amount) SOQL, queries current Account values, and only updates when total changed. 2 SOQL, 1 DML, fully bulkified."
Build a unique key = AccountId + '_' + Product2Id to check existing Assets. Add new keys to the Set inside the loop to also prevent intra-batch duplicates.
trigger AccountAssetCopyTrigger on Account (after update) { AccountAssetCopyHelper.copyOLIsToAssets(Trigger.new, Trigger.oldMap); } // Helper Steps: // 1. Collect Account Ids → Query Opportunities → Map<OppId, AccountId> // 2. Query OLIs WHERE OpportunityId IN oppIds AND Product2Id != null // 3. Query existing Assets → build Set of 'AccountId_Product2Id' keys // 4. Loop OLIs → check key → skip if exists → add to assetList + add key to Set // 5. Insert all Assets in one DML // Result: 3 SOQL, 1 DML, idempotent, intra-batch dedup handled
"I wrote an after update trigger on Account that queries Opportunities, OLIs with Product details, and existing Assets. I build a unique key Set using AccountId+'_'+Product2Id to prevent duplicates — also adding new keys inside the loop to prevent intra-batch duplicates. Assets created with Name, AccountId, Product2Id, Quantity, Price, Status=Purchased in one DML. 3 SOQL, 1 DML, idempotent."
trigger AccountBillingCityTrigger on Account (after update) { AccountBillingCityHelper.syncMailingCity(Trigger.new, Trigger.oldMap); } public class AccountBillingCityHelper { public static void syncMailingCity(List<Account> updatedAccounts, Map<Id, Account> oldMap) { Map<Id, String> accountCityMap = new Map<Id, String>(); for (Account acc : updatedAccounts) { if (acc.BillingCity != oldMap.get(acc.Id).BillingCity) accountCityMap.put(acc.Id, acc.BillingCity); } if (accountCityMap.isEmpty()) return; List<Contact> contacts = [ SELECT Id, AccountId, MailingCity FROM Contact WHERE AccountId IN :accountCityMap.keySet()]; if (contacts.isEmpty()) return; List<Contact> toUpdate = new List<Contact>(); for (Contact con : contacts) { String newCity = accountCityMap.get(con.AccountId); if (con.MailingCity != newCity) { con.MailingCity = newCity; toUpdate.add(con); } } if (!toUpdate.isEmpty()) update toUpdate; } }
con.MailingCity != newCity before updating?"I wrote an after update trigger on Account that uses oldMap to detect BillingCity changes, stores new city in a Map, queries all Contacts in one SOQL, compares MailingCity before updating — only changed Contacts get DML. 1 SOQL, 1 DML, fully bulkified."
trigger OpportunityClientContactTrigger on Opportunity (after update) { OpportunityClientContactHelper.syncClientContact(Trigger.new, Trigger.oldMap); } public class OpportunityClientContactHelper { public static void syncClientContact(List<Opportunity> updatedOpps, Map<Id, Opportunity> oldMap) { Map<Id, Id> accountContactMap = new Map<Id, Id>(); for (Opportunity opp : updatedOpps) { if (opp.Client_Contact__c != oldMap.get(opp.Id).Client_Contact__c && opp.AccountId != null) accountContactMap.put(opp.AccountId, opp.Client_Contact__c); } if (accountContactMap.isEmpty()) return; List<Account> toUpdate = new List<Account>(); for (Account acc : [SELECT Id, Client_Contact__c FROM Account WHERE Id IN :accountContactMap.keySet()]) { Id newContact = accountContactMap.get(acc.Id); if (acc.Client_Contact__c != newContact) { Account a = new Account(); a.Id = acc.Id; a.Client_Contact__c = newContact; toUpdate.add(a); } } if (!toUpdate.isEmpty()) update toUpdate; } }
"I wrote an after update trigger on Opportunity that uses oldMap to detect Client_Contact__c changes, stores new Contact Id in a Map keyed by AccountId, queries Account current values, and only updates if different. If multiple Opps on same Account change, last one wins — clarify priority with business. 1 SOQL, 1 DML, fully bulkified."
Never use MAX(Serial_No__c) — it resets on delete. Store the counter on the parent Opportunity in a Last_OLI_Serial_No__c field. Counter only goes up, never down.
// Fields needed: // OLI: Serial_No__c (Text) // Opportunity: Last_OLI_Serial_No__c (Number) trigger OLISerialNoTrigger on OpportunityLineItem (before insert) { OLISerialNoHelper.assignSerialNumbers(Trigger.new); } public class OLISerialNoHelper { public static void assignSerialNumbers(List<OpportunityLineItem> newOLIs) { Set<Id> oppIds = new Set<Id>(); for (OpportunityLineItem oli : newOLIs) oppIds.add(oli.OpportunityId); Map<Id, Opportunity> oppMap = new Map<Id, Opportunity>([ SELECT Id, Last_OLI_Serial_No__c FROM Opportunity WHERE Id IN :oppIds]); Map<Id, Integer> counterMap = new Map<Id, Integer>(); for (Opportunity opp : oppMap.values()) { counterMap.put(opp.Id, opp.Last_OLI_Serial_No__c != null ? Integer.valueOf(opp.Last_OLI_Serial_No__c) : 0); } for (OpportunityLineItem oli : newOLIs) { Integer counter = counterMap.get(oli.OpportunityId); counter++; oli.Serial_No__c = String.valueOf(counter); // before insert — no DML needed counterMap.put(oli.OpportunityId, counter); // update Map for next OLI in batch } List<Opportunity> toUpdate = new List<Opportunity>(); for (Id oppId : counterMap.keySet()) { Opportunity o = new Opportunity(); o.Id = oppId; o.Last_OLI_Serial_No__c = counterMap.get(oppId); toUpdate.add(o); } if (!toUpdate.isEmpty()) update toUpdate; } }
"I wrote a before insert trigger on OLI that queries the Opportunity counter, increments per OLI (updating the Map after each to handle bulk inserts correctly), sets Serial_No__c directly on Trigger.new — no OLI DML needed since it's before insert — then updates Opportunity counter in one DML. Counter never decreases on delete so sequence is always forward. 1 SOQL, 1 DML."
Insert: deduct full new quantity. Update: deduct only the difference (newQty - oldQty). Delete: restore full old quantity. Use System.TriggerOperation enum to handle each case cleanly.
trigger OLIProductQuantityTrigger on OpportunityLineItem (after insert, after update, after delete) { OLIProductQuantityHelper.updateProductQuantity( Trigger.new, Trigger.old, Trigger.oldMap, Trigger.operationType); } public class OLIProductQuantityHelper { public static void updateProductQuantity( List<OpportunityLineItem> newOLIs, List<OpportunityLineItem> oldOLIs, Map<Id, OpportunityLineItem> oldMap, System.TriggerOperation opType) { Map<Id, Decimal> productChangeMap = new Map<Id, Decimal>(); if (opType == System.TriggerOperation.AFTER_INSERT) { for (OpportunityLineItem oli : newOLIs) if (oli.Product2Id != null) addToMap(productChangeMap, oli.Product2Id, oli.Quantity != null ? oli.Quantity : 0); } else if (opType == System.TriggerOperation.AFTER_UPDATE) { for (OpportunityLineItem oli : newOLIs) { if (oli.Product2Id == null) continue; Decimal diff = (oli.Quantity != null ? oli.Quantity : 0) - (oldMap.get(oli.Id).Quantity != null ? oldMap.get(oli.Id).Quantity : 0); if (diff != 0) addToMap(productChangeMap, oli.Product2Id, diff); } } else if (opType == System.TriggerOperation.AFTER_DELETE) { for (OpportunityLineItem oli : oldOLIs) if (oli.Product2Id != null) addToMap(productChangeMap, oli.Product2Id, -(oli.Quantity != null ? oli.Quantity : 0)); } if (productChangeMap.isEmpty()) return; List<Product2> prods = [SELECT Id, Available_Quantity__c FROM Product2 WHERE Id IN :productChangeMap.keySet()]; List<Product2> toUpdate = new List<Product2>(); for (Product2 prod : prods) { Decimal curr = prod.Available_Quantity__c != null ? prod.Available_Quantity__c : 0; Decimal newAvail = Math.max(0, curr - productChangeMap.get(prod.Id)); Product2 p = new Product2(); p.Id = prod.Id; p.Available_Quantity__c = newAvail; toUpdate.add(p); } if (!toUpdate.isEmpty()) update toUpdate; } private static void addToMap(Map<Id, Decimal> m, Id id, Decimal qty) { if (!m.containsKey(id)) m.put(id, 0); m.put(id, m.get(id) + qty); } }
"I wrote an after insert/update/delete trigger on OLI using System.TriggerOperation enum. Insert deducts full quantity, update deducts only difference (newQty-oldQty), delete restores old quantity using negative values. All accumulated per Product in a Map using addToMap() helper. Math.max(0,...) prevents negative stock. 1 SOQL, 1 DML, fully bulkified."
OWD=Private means only owner can see records. To share programmatically, create Student__Share records (auto-generated by Salesforce when OWD=Private). Use RowCause = Manual and Database.insert(false) for partial success to handle duplicate sharing gracefully.
trigger StudentSharingTrigger on Student__c (after insert) { StudentSharingHelper.shareWithSalesforceUsers(Trigger.new); } public class StudentSharingHelper { public static void shareWithSalesforceUsers(List<Student__c> newStudents) { List<User> sfUsers = [ SELECT Id FROM User WHERE Profile.Name = 'Salesforce User' AND IsActive = true]; if (sfUsers.isEmpty()) return; List<Student__Share> shareRecords = new List<Student__Share>(); for (Student__c student : newStudents) { for (User usr : sfUsers) { if (usr.Id == student.OwnerId) continue; // Skip owner Student__Share sr = new Student__Share(); sr.ParentId = student.Id; sr.UserOrGroupId = usr.Id; sr.AccessLevel = 'Read'; sr.RowCause = Schema.Student__Share.RowCause.Manual; shareRecords.add(sr); } } if (!shareRecords.isEmpty()) Database.insert(shareRecords, false); // false = partial success } }
Database.insert(shareRecords, false)?"I created Student__c with OWD=Private — which auto-generates Student__Share. I wrote an after insert trigger that queries active Salesforce User profile users in one SOQL, creates Student__Share records with ParentId, UserOrGroupId, AccessLevel=Read, RowCause=Manual — skipping the Owner. Database.insert(false) handles duplicate shares gracefully. 1 SOQL, 1 DML, fully bulkified."
DML 1: Insert Opportunity. DML 2: Insert OLI (needs Opportunity Id). Cannot be combined. Use Map<AssetId, Opportunity> — after DML 1, Salesforce populates Opp Ids on the same object references in memory.
trigger AssetOpportunityTrigger on Asset (after insert) { AssetOpportunityHelper.createOpportunityAndOLI(Trigger.new); } // Helper Steps: // 1. Collect Product Ids → Query Standard Pricebook (Test.isRunningTest() guard) // 2. Query PricebookEntries → Map<Product2Id, PBE> // 3. Build Opps → Map<AssetId, Opportunity> → Insert (DML 1, Ids populated) // 4. Build OLIs using assetToOppMap.get(ast.Id).Id → Insert (DML 2) // Result: 2 SOQL, 2 DML — order is mandatory (OLI needs Opp Id)
"I wrote an after insert trigger on Asset. The helper queries Standard Pricebook and PricebookEntries, builds one Opportunity per Asset in a Map, inserts them (DML 1 — Ids now populated on same references), then loops Assets to build OLIs using assetToOppMap.get(ast.Id).Id and inserts in DML 2. Two DML calls are mandatory — OLI needs Opp Id which only exists after DML 1. 2 SOQL, 2 DML."
trigger UserUpdateEmailTrigger on User (after update) { UserUpdateEmailHelper.sendManagerEmail(Trigger.new, Trigger.oldMap); } // Helper Steps: // 1. Collect User Ids + Manager Ids (skip null ManagerId) // 2. Query Manager details → Map<ManagerId, User> // 3. Query Accounts owned by Users → Map<UserId, List<Account>> // 4. AggregateResult COUNT(Id) GROUP BY AccountId → Map<AccountId, Integer> // 5. Build HTML email per User showing: Total Accounts + table of Account→Contact count // 3 SOQL, 0 DML, 1 sendEmail call // Email Body: // Hi [Manager Name], // [User Name] has been updated. You have assigned [X] accounts. // Total Account : 3 // | Account Name | No. of Contacts | // | Ami Polymer | 10 |
"I wrote an after update trigger on User that queries Manager details, Accounts by OwnerId grouped in a Map, and Contact counts via AggregateResult COUNT(). Builds HTML email per User with Manager as recipient — showing total Accounts and per-Account Contact count table. 3 SOQL, 0 DML, 1 sendEmail call, fully bulkified."
trigger AccountUserCountTrigger on Account (after update, after delete) { if (Trigger.isUpdate) AccountUserCountHelper.incrementCount(Trigger.new); if (Trigger.isDelete) AccountUserCountHelper.decrementCount(Trigger.old); } public class AccountUserCountHelper { public static void incrementCount(List<Account> accounts) { Map<Id, Integer> userMap = buildCountMap(accounts); applyToUsers(userMap, true); } public static void decrementCount(List<Account> accounts) { Map<Id, Integer> userMap = buildCountMap(accounts); applyToUsers(userMap, false); } private static Map<Id, Integer> buildCountMap(List<Account> accounts) { Map<Id, Integer> m = new Map<Id, Integer>(); for (Account acc : accounts) { if (acc.OwnerId == null) continue; if (!m.containsKey(acc.OwnerId)) m.put(acc.OwnerId, 0); m.put(acc.OwnerId, m.get(acc.OwnerId) + 1); } return m; } private static void applyToUsers(Map<Id, Integer> userMap, Boolean increment) { List<User> users = [SELECT Id, Count__c FROM User WHERE Id IN :userMap.keySet()]; List<User> toUpdate = new List<User>(); for (User usr : users) { Decimal curr = usr.Count__c != null ? usr.Count__c : 0; Integer change = userMap.get(usr.Id); User u = new User(); u.Id = usr.Id; u.Count__c = increment ? curr + change : Math.max(0, curr - change); toUpdate.add(u); } if (!toUpdate.isEmpty()) update toUpdate; } }
"I wrote a single after update/delete trigger on Account routing to increment and decrement methods. Each method builds a Map of UserId→accountCount, queries current User Count__c, applies increment or decrement using Math.max(0,...) guard, and updates in one DML. 1 SOQL, 1 DML per operation, fully bulkified."
trigger OLIProductFamilyTrigger on OpportunityLineItem (before insert) { OLIProductFamilyHelper.validateProductFamily(Trigger.new); } public class OLIProductFamilyHelper { public static void validateProductFamily(List<OpportunityLineItem> newOLIs) { Set<Id> oppIds = new Set<Id>(); Set<Id> productIds = new Set<Id>(); for (OpportunityLineItem oli : newOLIs) { oppIds.add(oli.OpportunityId); productIds.add(oli.Product2Id); } Map<Id, Opportunity> oppMap = new Map<Id, Opportunity>([ SELECT Id, Product_Type__c FROM Opportunity WHERE Id IN :oppIds]); Map<Id, Product2> productMap = new Map<Id, Product2>([ SELECT Id, Family FROM Product2 WHERE Id IN :productIds]); for (OpportunityLineItem oli : newOLIs) { Opportunity opp = oppMap.get(oli.OpportunityId); Product2 prod = productMap.get(oli.Product2Id); if (opp == null || String.isBlank(opp.Product_Type__c)) continue; if (prod == null || String.isBlank(prod.Family)) { oli.addError('Product Family not set on Product.'); continue; } if (opp.Product_Type__c != prod.Family) oli.addError('Product Family mismatch! Opp type is "' + opp.Product_Type__c + '" but Product Family is "' + prod.Family + '".'); } } }
"I wrote a before insert trigger on OLI that queries Opportunities and Products into separate Maps, then loops Trigger.new comparing opp.Product_Type__c vs prod.Family. Mismatch triggers addError() with descriptive message showing both values. If Opp has no Product Type, validation is skipped. 2 SOQL, 0 DML, fully bulkified."
trigger OpportunityMinMaxDateTrigger on Opportunity (after insert, after update, after delete, after undelete) { List<Opportunity> oppList = Trigger.isDelete ? Trigger.old : Trigger.new; OpportunityMinMaxDateHelper.updateAccountMinMaxDate(oppList); } public class OpportunityMinMaxDateHelper { public static void updateAccountMinMaxDate(List<Opportunity> oppList) { Set<Id> accountIds = new Set<Id>(); for (Opportunity opp : oppList) if (opp.AccountId != null) accountIds.add(opp.AccountId); if (accountIds.isEmpty()) return; Map<Id, Date> minMap = new Map<Id, Date>(); Map<Id, Date> maxMap = new Map<Id, Date>(); for (Id id : accountIds) { minMap.put(id, null); maxMap.put(id, null); } for (AggregateResult ar : [ SELECT AccountId, MIN(CloseDate) minDate, MAX(CloseDate) maxDate FROM Opportunity WHERE AccountId IN :accountIds AND CloseDate != null GROUP BY AccountId]) { minMap.put((Id)ar.get('AccountId'), (Date)ar.get('minDate')); maxMap.put((Id)ar.get('AccountId'), (Date)ar.get('maxDate')); } List<Account> toUpdate = new List<Account>(); for (Account acc : [SELECT Id, Min_Date__c, Max_Date__c FROM Account WHERE Id IN :accountIds]) { if (acc.Min_Date__c != minMap.get(acc.Id) || acc.Max_Date__c != maxMap.get(acc.Id)) { Account a = new Account(); a.Id = acc.Id; a.Min_Date__c = minMap.get(acc.Id); a.Max_Date__c = maxMap.get(acc.Id); toUpdate.add(a); } } if (!toUpdate.isEmpty()) update toUpdate; } }
"I wrote an after insert/update/delete/undelete trigger on Opportunity. One AggregateResult SOQL gets MIN and MAX CloseDate per Account grouped together. Initialize all Accounts with null so deleted-Opp accounts get both dates cleared. Only fire DML when either date changed. 2 SOQL, 1 DML, fully bulkified."
Task.WhatId can link to Account, Opportunity, Case, or any object. Verify it's an Opportunity using the prefix '006'. Must query ALL Tasks per Opportunity — not just the updated ones.
trigger TaskCompletedTrigger on Task (after update) { TaskCompletedHelper.closeOpportunityIfAllTasksDone(Trigger.new, Trigger.oldMap); } public class TaskCompletedHelper { public static void closeOpportunityIfAllTasksDone(List<Task> updatedTasks, Map<Id, Task> oldMap) { Set<Id> oppIds = new Set<Id>(); for (Task tsk : updatedTasks) { Task old = oldMap.get(tsk.Id); if (tsk.Status == 'Completed' && old.Status != 'Completed' && tsk.WhatId != null && String.valueOf(tsk.WhatId).startsWith('006')) // Opp prefix oppIds.add(tsk.WhatId); } if (oppIds.isEmpty()) return; // Query ALL Tasks per Opp — not just updated ones Map<Id, List<Task>> oppTaskMap = new Map<Id, List<Task>>(); for (Task t : [SELECT Id, WhatId, Status FROM Task WHERE WhatId IN :oppIds]) { if (!oppTaskMap.containsKey(t.WhatId)) oppTaskMap.put(t.WhatId, new List<Task>()); oppTaskMap.get(t.WhatId).add(t); } Set<Id> oppsToClose = new Set<Id>(); for (Id oppId : oppTaskMap.keySet()) { Boolean allDone = true; for (Task t : oppTaskMap.get(oppId)) { if (t.Status != 'Completed') { allDone = false; break; } } if (allDone) oppsToClose.add(oppId); } if (oppsToClose.isEmpty()) return; List<Opportunity> toUpdate = new List<Opportunity>(); for (Opportunity opp : [SELECT Id FROM Opportunity WHERE Id IN :oppsToClose AND StageName != 'Closed Won']) { Opportunity o = new Opportunity(); o.Id = opp.Id; o.StageName = 'Closed Won'; toUpdate.add(o); } if (!toUpdate.isEmpty()) update toUpdate; } }
"I wrote an after update trigger on Task using oldMap to detect Status changing TO Completed and WhatId prefix '006' for Opportunity validation. I query ALL Tasks per Opportunity, group by WhatId, check if all are Completed using early break optimization, then update qualifying Opportunity Stages to Closed Won. 2 SOQL, 1 DML, fully bulkified."
trigger AccountChildDeleteTrigger on Account (before delete) { AccountChildDeleteHelper.preventDeletion(Trigger.old); } public class AccountChildDeleteHelper { public static void preventDeletion(List<Account> deletedAccounts) { Set<Id> accountIds = new Set<Id>(); for (Account acc : deletedAccounts) accountIds.add(acc.Id); Map<Id, List<String>> blockMap = new Map<Id, List<String>>(); collectReasons([SELECT AccountId, COUNT(Id) total FROM Contact WHERE AccountId IN :accountIds GROUP BY AccountId], blockMap, 'Contact'); collectReasons([SELECT AccountId, COUNT(Id) total FROM Opportunity WHERE AccountId IN :accountIds GROUP BY AccountId], blockMap, 'Opportunity'); collectReasons([SELECT AccountId, COUNT(Id) total FROM Case WHERE AccountId IN :accountIds GROUP BY AccountId], blockMap, 'Case'); collectReasons([SELECT AccountId, COUNT(Id) total FROM Asset WHERE AccountId IN :accountIds GROUP BY AccountId], blockMap, 'Asset'); for (Account acc : deletedAccounts) { if (blockMap.containsKey(acc.Id)) acc.addError('Cannot delete — Child records exist: ' + String.join(blockMap.get(acc.Id), ' | ') + '. Please remove all child records first.'); } } private static void collectReasons(List<AggregateResult> results, Map<Id, List<String>> blockMap, String label) { for (AggregateResult ar : results) { Id accId = (Id)ar.get('AccountId'); if ((Integer)ar.get('total') > 0) { if (!blockMap.containsKey(accId)) blockMap.put(accId, new List<String>()); blockMap.get(accId).add(label + '(' + ar.get('total') + ')'); } } } }
"I wrote a before delete trigger on Account using Trigger.old (Trigger.new is null in delete). 4 AggregateResult COUNT() SOQLs check Contacts, Opps, Cases, Assets — all routed through a reusable collectReasons() helper. addError() shows exactly which objects are blocking with counts like Contact(3)|Opportunity(2). 4 SOQL, 0 DML, fully bulkified."
GroupMember is a junction object linking Users to Public Groups. Fields: GroupId (the group), UserOrGroupId (the user). On insert: check if profile is Platform User. On update: check if profile specifically CHANGED TO Platform User using oldMap. Always check existing members to prevent duplicate DML errors.
trigger UserProfileGroupTrigger on User (after insert, after update) { UserProfileGroupHelper.addToTestGroup(Trigger.new, Trigger.oldMap); } public class UserProfileGroupHelper { public static void addToTestGroup(List<User> users, Map<Id, User> oldMap) { List<Profile> profileList = [SELECT Id FROM Profile WHERE Name = 'Platform User' LIMIT 1]; if (profileList.isEmpty()) return; Id platformProfileId = profileList[0].Id; Set<Id> eligibleUserIds = new Set<Id>(); for (User usr : users) { if (oldMap == null) { // INSERT if (usr.ProfileId == platformProfileId) eligibleUserIds.add(usr.Id); } else { // UPDATE — only if profile CHANGED to Platform User if (usr.ProfileId == platformProfileId && oldMap.get(usr.Id).ProfileId != platformProfileId) eligibleUserIds.add(usr.Id); } } if (eligibleUserIds.isEmpty()) return; List<Group> groupList = [SELECT Id FROM Group WHERE DeveloperName = 'Test_Group' AND Type = 'Regular' LIMIT 1]; if (groupList.isEmpty()) return; Id testGroupId = groupList[0].Id; Set<Id> alreadyInGroup = new Set<Id>(); for (GroupMember gm : [SELECT UserOrGroupId FROM GroupMember WHERE GroupId = :testGroupId AND UserOrGroupId IN :eligibleUserIds]) alreadyInGroup.add(gm.UserOrGroupId); List<GroupMember> toInsert = new List<GroupMember>(); for (Id userId : eligibleUserIds) { if (alreadyInGroup.contains(userId)) continue; GroupMember gm = new GroupMember(); gm.GroupId = testGroupId; gm.UserOrGroupId = userId; toInsert.add(gm); } if (!toInsert.isEmpty()) insert toInsert; } }
"I wrote an after insert/update trigger on User that queries Platform User profile Id, filters users whose profile matches (insert) or specifically changed to it (update via oldMap). Queries Test_Group by DeveloperName with Type=Regular, checks existing GroupMembers to prevent duplicates, then inserts GroupMember records. 3 SOQL, 1 DML, fully bulkified. Enhancement: handle profile changing AWAY from Platform User by deleting GroupMember."
Salesforce is a multi-tenant platform — all companies share the same infrastructure. Governor Limits prevent any single org from consuming all resources.
Trigger Batch Size
Triggers process records in batches of 200. If you insert 1000 records, the trigger fires 5 times (200×5). Each batch is a separate transaction with fresh governor limits.
// ── DML Limits ──────────────────────────────────────── Trigger Batch Size = 200 records per execution Total DML Statements = 150 per transaction Total DML Rows = 10,000 per transaction // ── SOQL Limits ─────────────────────────────────────── Total SOQL Queries (Sync) = 100 Total SOQL Queries (Async) = 200 Total SOQL Rows = 50,000 Batch Apex getQueryLocator = 50,000,000 // ── CPU & Memory ────────────────────────────────────── CPU Time (Sync) = 10,000 ms CPU Time (Async) = 60,000 ms Heap Size (Sync) = 6 MB Heap Size (Async) = 12 MB // ── Email ───────────────────────────────────────────── sendEmail() calls = 10 per transaction // ── Check usage programmatically ────────────────────── System.debug(Limits.getDmlStatements()); // DML used System.debug(Limits.getQueries()); // SOQL used System.debug(Limits.getCpuTime()); // CPU used
"Trigger batch size is 200 per execution. Key limits per transaction: 150 DML statements, 10,000 DML rows, 100 SOQL queries, 50,000 SOQL rows, 10,000ms CPU time. Most common violation is SOQL or DML inside a loop. Fix: collect in Sets/Maps/Lists, fire SOQL and DML once outside the loop. For large data beyond sync limits, use Queueable (200 SOQL, 60,000ms CPU) or Batch Apex. Monitor with Limits class."
Error Message
System.LimitException: Apex CPU time limit exceeded
Sync limit: 10,000ms | Async limit: 60,000ms
CPU Time = time executing code (SOQL wait time is EXCLUDED)
// ❌ O(n²) — 200×200 = 40,000 iterations for (Account acc : accounts) { for (Contact con : contacts) { // CPU explodes! if (con.AccountId == acc.Id) { } } } // ✅ O(n) — build Map once, O(1) lookup Map<Id, List<Contact>> accConMap = new Map<Id, List<Contact>>(); for (Contact con : contacts) { if (!accConMap.containsKey(con.AccountId)) accConMap.put(con.AccountId, new List<Contact>()); accConMap.get(con.AccountId).add(con); } for (Account acc : accounts) { List<Contact> cons = accConMap.get(acc.Id); // O(1) ✅ }
// ❌ Creates new String object each iteration String result = ''; for (Integer i = 0; i < 10000; i++) result += 'Item ' + i; // ✅ Collect in List — join once List<String> parts = new List<String>(); for (Integer i = 0; i < 10000; i++) parts.add('Item ' + i); String result = String.join(parts, ', ');
if (TriggerHelper.isExecuting) return; before logic. Set to true before DML, false after. Static variables persist within the same transaction — prevents the trigger from calling itself."CPU timeout means Apex code consumed more than 10,000ms. Most common cause: nested loops — replacing them with Map lookups drops complexity from O(n²) to O(n) instantly. Other fixes: move SOQL outside loops, use String.join() instead of concatenation, static boolean recursion guard, and for large data — Queueable Apex with 60,000ms CPU limit. I use Limits.getCpuTime() during testing to find bottlenecks."
Error Message
System.LimitException: Too many SOQL queries: 101
Sync: 100 queries max | Async: 200 queries max
// ❌ 200 Accounts = 200 SOQL = Exception on 101st for (Account acc : Trigger.new) { List<Contact> cons = [SELECT Id FROM Contact WHERE AccountId = :acc.Id]; } // ✅ 1 SOQL for ALL Accounts Set<Id> accountIds = new Set<Id>(); for (Account acc : Trigger.new) accountIds.add(acc.Id); Map<Id, List<Contact>> accConMap = new Map<Id, List<Contact>>(); for (Contact con : [SELECT Id, AccountId FROM Contact WHERE AccountId IN :accountIds]) { if (!accConMap.containsKey(con.AccountId)) accConMap.put(con.AccountId, new List<Contact>()); accConMap.get(con.AccountId).add(con); }
"SOQL 101 means more than 100 queries fired in one transaction. Most common cause: SOQL inside a for loop. Fix: collect all Ids into a Set first, fire one SOQL using IN :setOfIds, build a Map for O(1) lookup. For helper methods called in loops, refactor them to accept a Set and return a Map. When Flow and Trigger run together, they share the same 100 limit — reduce Get Records in Flow. Use Queueable for async (200 SOQL limit). Monitor with Limits.getQueries()."
Error Message
System.NullPointerException: Attempt to de-reference a null object
Happens when you try to access a method or property on a variable that is null.
// ❌ Direct SOQL to SObject — null if no record Account acc = [SELECT Id FROM Account WHERE Name = 'XYZ' LIMIT 1]; acc.Name.length(); // NPE if no record found! // ✅ Always use List → check isEmpty() List<Account> accs = [SELECT Id, Name FROM Account WHERE Name = 'XYZ' LIMIT 1]; if (!accs.isEmpty()) Integer len = accs[0].Name.length();
// ❌ Account not in query — con.Account is null String name = con.Account.Name; // NPE! // ✅ Include in SOQL + check List<Contact> cons = [SELECT Id, Account.Name FROM Contact...]; if (con.Account != null) String name = con.Account.Name;
// ❌ Email could be null con.Email.contains('@gmail.com'); // NPE! // ✅ String.isNotBlank() check if (String.isNotBlank(con.Email)) con.Email.contains('@gmail.com'); // ✅ Safe navigation operator — cleanest approach Boolean isGmail = con.Email?.contains('@gmail.com'); // returns null if Email null String city = con.Account?.BillingAddress?.city; // chain safely
Trigger.new is always null in delete triggers. Always use Trigger.old in delete context — it holds the records being deleted.Decimal amount = opp.Amount != null ? opp.Amount : 0; before any arithmetic. Never do opp.Amount * 0.18 directly if Amount could be null."Null Pointer Exception means code tried to access a property or call a method on a null variable. Common causes: single-record SOQL returning no results (fix: use List + isEmpty()), relationship field not in SOQL query, Map.get() returning null, Trigger.new in delete context (fix: use Trigger.old), and string methods on null fields. My go-to fix is the safe navigation operator ?. introduced in Spring 21 — opp.Account?.Client_Contact__r?.Email returns null safely instead of crashing. I use Debug Logs to find the exact line, then add System.debug() to identify the null variable."