Welcome to Salesforce Hours!
If you’re looking to sharpen your Salesforce development skills with real-world examples and practical tutorials, you’re in the right place.
In this Part 1 of our “Salesforce Apex Trigger Scenarios“, we’re diving into some of the most common and sometimes tricky use cases you’ll come across when working with Apex triggers. These examples are built around standard Salesforce objects and designed to mirror the actual interview question that are asked in most of the Salesforce interview .
You’ll find clean, scalable Apex code solutions that not only solve problems but also follow best practices. Whether you’re just getting started or refining your skills, this guide is here to help you grow confidently as a Salesforce developer.
Scenario 1: Update Account Type When Opportunity is Closed Won
Requirement: When an Opportunity is marked as Closed Won, the related Account’s Type field should be updated to “Customer”.
Trigger Code
trigger OpportunityTrigger on Opportunity (after update) {
if (TriggerHandlerUtility.isFirstRun) {
if (Trigger.isAfter && Trigger.isUpdate) {
OpportunityTriggerHandler.updateAccountType(Trigger.new, Trigger.oldMap);
}
}
}
Handler Class
public class OpportunityTriggerHandler {
public static void updateAccountType(List<Opportunity> newList, Map<Id, Opportunity> oldMap) {
Set<Id> accountIds = new Set<Id>();
for (Opportunity opp : newList) {
Opportunity oldOpp = oldMap.get(opp.Id);
if (opp.StageName == 'Closed Won' && oldOpp.StageName != 'Closed Won') {
if (opp.AccountId != null) {
accountIds.add(opp.AccountId);
}
}
}
List<Account> accsToUpdate = [SELECT Id, Type FROM Account WHERE Id IN :accountIds];
for (Account acc : accsToUpdate) {
acc.Type = 'Customer';
}
if (!accsToUpdate.isEmpty()) {
update accsToUpdate;
}
}
}
Recursion Utility
public class TriggerHandlerUtility {
public static Boolean isFirstRun = true;
}
Scenario 2: Prevent Contact Creation Without Account
Requirement: Users should not be able to create a Contact without linking it to an Account.
Trigger Code
trigger ContactTrigger on Contact (before insert) {
for (Contact con : Trigger.new) {
if (con.AccountId == null) {
con.addError('Please link this Contact to an Account before saving.');
}
}
}
Scenario 3: Roll Up Total Opportunity Amount to Account
Requirement: Automatically update the custom field Total_Opportunity_Amount__c on Account whenever Opportunities are added, updated, deleted, or undeleted.
Trigger Code
trigger OpportunityTrigger on Opportunity (after insert, after update, after delete, after undelete) {
if (TriggerHandlerUtility.isFirstRun) {
OpportunityTriggerHandler.rollupOpportunityAmount(
Trigger.newMap,
Trigger.oldMap,
Trigger.isDelete
);
}
}
Handler Method
public class OpportunityTriggerHandler {
public static void rollupOpportunityAmount(Map<Id, Opportunity> newMap, Map<Id, Opportunity> oldMap, Boolean isDelete) {
Set<Id> accountIds = new Set<Id>();
if (isDelete) {
for (Opportunity opp : oldMap.values()) {
if (opp.AccountId != null) accountIds.add(opp.AccountId);
}
} else {
for (Opportunity opp : newMap.values()) {
if (opp.AccountId != null) accountIds.add(opp.AccountId);
}
}
Map<Id, Decimal> totalMap = new Map<Id, Decimal>();
for (AggregateResult ar : [
SELECT AccountId, SUM(Amount) totalAmount
FROM Opportunity
WHERE AccountId IN :accountIds
GROUP BY AccountId
]) {
totalMap.put((Id) ar.get('AccountId'), (Decimal) ar.get('totalAmount'));
}
List<Account> accountsToUpdate = new List<Account>();
for (Id accId : accountIds) {
Decimal total = totalMap.containsKey(accId) ? totalMap.get(accId) : 0;
accountsToUpdate.add(new Account(Id = accId, Total_Opportunity_Amount__c = total));
}
update accountsToUpdate;
}
}
Scenario 4: Clone OpportunityLineItems When Opportunity is Cloned
Requirement: When a user clones an Opportunity, also clone its associated OpportunityLineItems. A custom field Clone_Of__c tracks the original Opportunity.
Trigger Code
trigger OpportunityTrigger on Opportunity (after insert) {
OpportunityTriggerHandler.cloneOpportunityLineItems(Trigger.new);
}
Handler Method
public class OpportunityTriggerHandler {
public static void cloneOpportunityLineItems(List<Opportunity> newOpps) {
Map<Id, Id> clonedMap = new Map<Id, Id>();
for (Opportunity opp : newOpps) {
if (opp.Clone_Of__c != null) {
clonedMap.put(opp.Clone_Of__c, opp.Id);
}
}
if (clonedMap.isEmpty()) return;
List<OpportunityLineItem> newLines = new List<OpportunityLineItem>();
for (OpportunityLineItem oli : [
SELECT OpportunityId, Quantity, UnitPrice, PricebookEntryId
FROM OpportunityLineItem
WHERE OpportunityId IN :clonedMap.keySet()
]) {
newLines.add(new OpportunityLineItem(
OpportunityId = clonedMap.get(oli.OpportunityId),
Quantity = oli.Quantity,
UnitPrice = oli.UnitPrice,
PricebookEntryId = oli.PricebookEntryId
));
}
insert newLines;
}
}
Scenario 5: Count Contacts Under Each Account
Requirement: Update a custom field Contact_Count__c on Account whenever Contacts are inserted, deleted, or undeleted.
Trigger Code
trigger ContactTrigger on Contact (after insert, after delete, after undelete) {
ContactTriggerHandler.updateContactCount(Trigger.newMap, Trigger.oldMap, Trigger.isDelete);
}
Handler Method
public class ContactTriggerHandler {
public static void updateContactCount(Map<Id, Contact> newMap, Map<Id, Contact> oldMap, Boolean isDelete) {
Set<Id> accountIds = new Set<Id>();
if (isDelete) {
for (Contact con : oldMap.values()) {
if (con.AccountId != null) accountIds.add(con.AccountId);
}
} else {
for (Contact con : newMap.values()) {
if (con.AccountId != null) accountIds.add(con.AccountId);
}
}
List<Account> accountsToUpdate = new List<Account>();
for (AggregateResult ar : [
SELECT AccountId, COUNT(Id) contactCount
FROM Contact
WHERE AccountId IN :accountIds
GROUP BY AccountId
]) {
accountsToUpdate.add(new Account(
Id = (Id) ar.get('AccountId'),
Contact_Count__c = (Integer) ar.get('contactCount')
));
}
update accountsToUpdate;
}
}
Scenario 6: Update Account Rating for High-Value Opportunities
Requirement: When a new Opportunity with an Amount exceeding 100,000 is created, update the related Account’s Rating to “Hot”.
Trigger Code
trigger OpportunityTrigger on Opportunity (after insert) {
OpportunityTriggerHandler.updateAccountRatingForHighValueOpps(Trigger.new);
}
Handler Method
public class OpportunityTriggerHandler {
public static void updateAccountRatingForHighValueOpps(List<Opportunity> newOpps) {
Set<Id> accountIds = new Set<Id>();
for (Opportunity opp : newOpps) {
if (opp.Amount != null && opp.Amount > 100000 && opp.AccountId != null) {
accountIds.add(opp.AccountId);
}
}
List<Account> accountsToUpdate = [SELECT Id, Rating FROM Account WHERE Id IN :accountIds];
for (Account acc : accountsToUpdate) {
acc.Rating = 'Hot';
}
if (!accountsToUpdate.isEmpty()) {
update accountsToUpdate;
}
}
}
Scenario 7: Sync Primary Contact Phone to Account
Requirement: When a Contact is flagged as Primary__c = true, copy its Phone number to the parent Account record.
Trigger Code
trigger ContactTrigger on Contact (after insert, after update) {
ContactTriggerHandler.syncPrimaryContactPhoneToAccount(Trigger.new);
}
Handler Method
public class ContactTriggerHandler {
public static void syncPrimaryContactPhoneToAccount(List<Contact> contacts) {
Map<Id, String> phoneMap = new Map<Id, String>();
for (Contact con : contacts) {
if (con.Primary__c && con.AccountId != null && con.Phone != null) {
phoneMap.put(con.AccountId, con.Phone);
}
}
List<Account> accs = [SELECT Id, Phone FROM Account WHERE Id IN :phoneMap.keySet()];
for (Account acc : accs) {
acc.Phone = phoneMap.get(acc.Id);
}
if (!accs.isEmpty()) update accs;
}
}
Scenario 8: Display Open Case Count on Opportunity
Requirement : Whenever a Case is inserted, updated, deleted, or undeleted, update the related Opportunity’s custom field Open_Case_Count__c to reflect the current number of open cases.
Trigger Code
trigger CaseTrigger on Case (after insert, after update, after delete, after undelete) {
CaseTriggerHandler.updateOpportunityCaseCount(Trigger.newMap, Trigger.oldMap, Trigger.isDelete);
}
Handler Method
public class CaseTriggerHandler {
public static void updateOpportunityCaseCount(Map<Id, Case> newMap, Map<Id, Case> oldMap, Boolean isDelete) {
Set<Id> oppIds = new Set<Id>();
if (isDelete) {
for (Case c : oldMap.values()) if (c.OpportunityId != null) oppIds.add(c.OpportunityId);
} else {
for (Case c : newMap.values()) if (c.OpportunityId != null) oppIds.add(c.OpportunityId);
}
Map<Id, Integer> countMap = new Map<Id, Integer>();
for (AggregateResult ar : [
SELECT OpportunityId, COUNT(Id) count FROM Case
WHERE OpportunityId IN :oppIds AND Status != 'Closed'
GROUP BY OpportunityId
]) {
countMap.put((Id)ar.get('OpportunityId'), (Integer)ar.get('count'));
}
List<Opportunity> oppsToUpdate = new List<Opportunity>();
for (Id oppId : oppIds) {
Integer openCount = countMap.containsKey(oppId) ? countMap.get(oppId) : 0;
oppsToUpdate.add(new Opportunity(Id = oppId, Open_Case_Count__c = openCount));
}
if (!oppsToUpdate.isEmpty()) update oppsToUpdate;
}
}
Scenario 9: Copy Matched Contact Email to New Leads
Requirement: Upon Lead creation via web-to-lead, if a Contact exists with the same FirstName and LastName, copy that Contact’s Email into Matched_Contact_Email__c on the Lead.
Trigger Code
trigger LeadTrigger on Lead (before insert) {
LeadTriggerHandler.matchAndSetContactEmail(Trigger.new);
}
Handler Method
public class LeadTriggerHandler {
public static void matchAndSetContactEmail(List<Lead> leads) {
Map<String, Lead> leadMap = new Map<String, Lead>();
for (Lead l : leads) {
if (l.FirstName != null && l.LastName != null) {
leadMap.put(l.FirstName + ' ' + l.LastName, l);
}
}
for (Contact c : [SELECT FirstName, LastName, Email FROM Contact WHERE FirstName != null AND LastName != null]) {
String key = c.FirstName + ' ' + c.LastName;
if (leadMap.containsKey(key)) {
leadMap.get(key).Matched_Contact_Email__c = c.Email;
}
}
}
}
Scenario 10: Append Industry Changes to Contact Descriptions
Requirement: When an Account’s Industry field is updated, append the new industry value to the Description of all related Contacts.
Trigger Code
trigger AccountTrigger on Account (after update) {
AccountTriggerHandler.appendIndustryToContactDescription(Trigger.new, Trigger.oldMap);
}
Handler Method
public class AccountTriggerHandler {
public static void appendIndustryToContactDescription(List<Account> accounts, Map<Id, Account> oldMap) {
Set<Id> accountIds = new Set<Id>();
for (Account acc : accounts) {
Account oldAcc = oldMap.get(acc.Id);
if (acc.Industry != oldAcc.Industry && acc.Industry != null) {
accountIds.add(acc.Id);
}
}
if (accountIds.isEmpty()) return;
List<Contact> contactsToUpdate = [SELECT Id, Description, AccountId FROM Contact WHERE AccountId IN :accountIds];
for (Contact c : contactsToUpdate) {
String prefix = c.Description != null ? c.Description + ' | ' : '';
c.Description = prefix + 'Industry changed to: ' + accountsMap.get(c.AccountId).Industry;
}
if (!contactsToUpdate.isEmpty()) update contactsToUpdate;
}
}
Scenario 11: Prevent Changing Close Date on Closed Opportunities
Requirement: Once an Opportunity is marked as “Closed Won” or “Closed Lost,” the close date must remain locked.
trigger OpportunityTrigger on Opportunity (before update) {
for (Opportunity opp : Trigger.new) {
Opportunity oldOpp = Trigger.oldMap.get(opp.Id);
if ((oldOpp.StageName == 'Closed Won' || oldOpp.StageName == 'Closed Lost') &&
opp.CloseDate != oldOpp.CloseDate) {
opp.CloseDate = oldOpp.CloseDate;
}
}
}
Scenario 12: Allow Only Managers to Update the Account Rating
Requirement: Only users whose title is “Manager” should be able to modify the “Rating” field on Account records.
trigger AccountTrigger on Account (before update) {
User currentUser = [SELECT Title FROM User WHERE Id = :UserInfo.getUserId() LIMIT 1];
for (Account acc : Trigger.new) {
Account oldAcc = Trigger.oldMap.get(acc.Id);
if (acc.Rating != oldAcc.Rating && currentUser.Title != 'Manager') {
acc.Rating = oldAcc.Rating;
}
}
}
Scenario 13: Block Contact Deletion If Related Opportunities Exist
Requirement: Prevent deletion of any Contact that is linked to at least one Opportunity.
trigger ContactTrigger on Contact (before delete) {
Set<Id> contactIds = new Set<Id>();
for (Contact con : Trigger.old) {
contactIds.add(con.Id);
}
Map<Id, Integer> contactOppCount = new Map<Id, Integer>();
for (AggregateResult ar : [
SELECT ContactId, COUNT(Id) cnt FROM Opportunity
WHERE ContactId IN :contactIds
GROUP BY ContactId
]) {
contactOppCount.put((Id)ar.get('ContactId'), (Integer)ar.get('cnt'));
}
for (Contact con : Trigger.old) {
if (contactOppCount.containsKey(con.Id)) {
con.addError('This contact cannot be deleted because related opportunities exist.');
}
}
}
Scenario 14: Reopen Case Automatically When Checkbox Is Checked
Requirement: If a custom checkbox field “Reopened__c” is checked, set the Case status to “Reopened.”
trigger CaseTrigger on Case (before update) {
for (Case c : Trigger.new) {
Case oldCase = Trigger.oldMap.get(c.Id);
if (c.Reopened__c == true && oldCase.Reopened__c != true) {
c.Status = 'Reopened';
}
}
}
Scenario 15: Prevent More Than One Primary Contact per Account
Requirement: Only one contact per Account should be marked as Primary__c = true, including new inserts or updates.
trigger ContactTrigger on Contact (before insert, before update) {
Set<Id> accountIds = new Set<Id>();
for (Contact con : Trigger.new) {
if (con.Primary__c == true && con.AccountId != null) {
accountIds.add(con.AccountId);
}
}
Map<Id, Integer> primaryCountMap = new Map<Id, Integer>();
for (AggregateResult ar : [
SELECT AccountId, COUNT(Id) cnt FROM Contact
WHERE Primary__c = true AND AccountId IN :accountIds
GROUP BY AccountId
]) {
primaryCountMap.put((Id)ar.get('AccountId'), (Integer)ar.get('cnt'));
}
// Simulate current transaction's changes
Map<Id, Integer> batchPrimaryCount = new Map<Id, Integer>();
for (Contact con : Trigger.new) {
if (con.Primary__c == true && con.AccountId != null) {
batchPrimaryCount.put(con.AccountId, batchPrimaryCount.getOrDefault(con.AccountId, 0) + 1);
}
}
for (Contact con : Trigger.new) {
if (con.Primary__c == true && con.AccountId != null) {
Integer existingCount = primaryCountMap.getOrDefault(con.AccountId, 0);
Integer currentCount = batchPrimaryCount.get(con.AccountId);
if (existingCount + currentCount > 1) {
con.addError('Only one primary contact is allowed per account.');
}
}
}
}
Summary
We hope this guide strengthens your Salesforce skill set and prepares you for challenging development scenarios and interviews. Keep innovating, stay curious, and follow SalesforceHours for more in-depth tutorials, code snippets, and real-world best practices.
Also read out once about the Execution Governors and Limits of Apex to avoid any error.
Comment for PART 2
——————————————————————————————————————————————-
LATEST POST TO CHECKOUT