This guide walks through 79 intermediate and advanced trigger challenges — rollups, recursion guards, bulkification, and multi-object trigger handlers. Each entry below includes the full problem statement, the governor-limit and best-practice constraints it's testing, and a numbered approach to solving it — then you can open the same problem in ApexArena's browser-based Apex editor and get instant pass/fail feedback against real test cases.
Medium
TriggersSOQL
1. Contact Count Rollup Trigger
Problem #2 · Salesforce Apex Coding Challenge
Problem Statement
After inserting or deleting a Contact, update a custom field
Contact_Count__c on the parent Account.
The field must always reflect the actual count of contacts linked to that account.
Constraints
- Must handle bulk (up to 200 contacts at once)
- Only one SOQL query and one DML statement total
- Handle contacts without an AccountId gracefully
Approach
- 1Use: SELECT AccountId, COUNT(Id) total FROM Contact WHERE AccountId IN :accountIds GROUP BY AccountId
- 2Build a Map from the AggregateResult before updating accounts.
- 3Update all accounts in a single List — one DML call only.
Medium
Triggers
2. Recursive Trigger Guard
Problem #7 · Salesforce Apex Coding Challenge
Problem Statement
Prevent a trigger from running more than once per transaction by implementing a
static boolean guard in a handler class.
This is critical when your trigger performs DML that could re-fire the same trigger,
causing infinite recursion and hitting the governor limit on recursive trigger depth (16).
Requirements
- Create a
TriggerHelper class with a static boolean
- The trigger must check and set the guard on entry
- Write a test proving the trigger runs exactly once even when the DML re-fires it
Approach
- 1Set hasRun = true BEFORE the DML that would re-trigger, not after.
- 2Reset hasRun = false in the @TestSetup or between test methods if testing multiple scenarios.
- 3For multi-object scenarios, use a Set of trigger names instead of a single boolean.
Medium
TriggersSOQL
3. Contact Email Deduplication
Problem #15 · Salesforce Apex Coding Challenge
Problem Statement
Write a before insert trigger on Contact that prevents
inserting a contact whose Email already exists on another contact record.
Requirements
- Collect all incoming emails in a
Set
- Run one SOQL query to find existing contacts
- Call
addError() on the Email field for duplicates
- Skip contacts with no email gracefully
Approach
- 1Query: SELECT Email FROM Contact WHERE Email IN :emails
- 2Build a Set from the query results, then check each new contact against it.
- 3Call c.Email.addError('A contact with this email already exists.')
Medium
TriggersSOQL
4. Opportunity Won Revenue Rollup
Problem #17 · Salesforce Apex Coding Challenge
Problem Statement
Write an after update trigger on Opportunity.
When an Opportunity's StageName changes to Closed Won,
recalculate the parent Account.AnnualRevenue as the
sum of all Closed Won opportunity amounts for that account.
Requirements
- Use
Trigger.old to detect the stage change
- One aggregate SOQL query with
SUM(Amount) and GROUP BY AccountId
- One DML
update on the affected accounts
Approach
- 1SOQL: SELECT AccountId, SUM(Amount) total FROM Opportunity WHERE AccountId IN :accountIds AND StageName = 'Closed Won' GROUP BY AccountId
- 2Use ar.get('AccountId') and ar.get('total') to read aggregate results.
- 3Build a Map then update Account.AnnualRevenue in a single DML.
Medium
TriggersSOQL
5. Prevent Duplicate Contacts by Email
Problem #22 · Salesforce Apex Coding Challenge
Problem Statement
Write a before insert trigger on Contact that prevents duplicate contacts
with the same Email. If a duplicate is detected, add an error on the Email field.
Constraints
- Must be bulkified — collect all emails, query once
- Use
addError() on the specific field
- Ignore contacts with a blank/null email
Approach
- 1Query: SELECT Email FROM Contact WHERE Email IN :emails
- 2Build a Set of existing emails, then loop Trigger.new to addError on matches.
Medium
TriggersSOQL
6. Auto-Close Stale Opportunities
Problem #25 · Salesforce Apex Coding Challenge
Problem Statement
Write an after insert trigger on Task. When a Task with
Subject = 'Close Opportunity' is inserted, set the linked Opportunity's
StageName to "Closed Lost".
Use WhatId to find the Opportunity Id.
Constraints
- Only process Tasks where Subject = 'Close Opportunity'
- Bulkified: one SOQL query, one DML update
Approach
- 1Query: SELECT Id, StageName FROM Opportunity WHERE Id IN :oppIds
- 2Loop the results, set StageName = 'Closed Lost', then call update opps.
Medium
TriggersSOQL
7. Case Count Rollup on Account
Problem #37 · Salesforce Apex Coding Challenge
Problem Statement
Write an after insert, after update, after delete trigger on Case
that keeps a custom field Open_Cases__c on Account updated with the
count of open (non-Closed) Cases for that account.
Constraints
- Bulkified — collect Account IDs, one aggregate SOQL, one update
- Status != 'Closed' for open cases
Approach
- 1Use AggregateResult: SELECT AccountId, COUNT(Id) cnt FROM Case WHERE AccountId IN :accountIds AND Status != 'Closed' GROUP BY AccountId
- 2Build a Map from the aggregate result, then loop accountIds to build update list.
Medium
Triggers
8. Dynamic Required Field Enforcement
Problem #44 · Salesforce Apex Coding Challenge
Problem Statement
Write a before insert, before update trigger on Contact
that enforces a custom rule: if MailingCountry = 'US', the MailingState
must not be blank.
Add a field error on MailingState if the rule is violated.
Approach
- 1Check: c.MailingCountry == 'US' && String.isBlank(c.MailingState)
- 2c.MailingState.addError('State is required for US addresses.');
Medium
TriggersSOQL
9. Sync Account Phone to Child Contacts
Problem #47 · Salesforce Apex Coding Challenge
Problem Statement
Write an after update trigger on Account that propagates
the Account's Phone to all child Contacts' Phone field
whenever the Account's Phone changes.
Constraints
- Only process Accounts where Phone has changed (
Trigger.oldMap)
- Bulkified — collect IDs, query once, one update DML
Approach
- 1Build a Map of accountId → new phone from Trigger.new.
- 2Query: SELECT Id, AccountId FROM Contact WHERE AccountId IN :changedIds
- 3For each Contact, set Phone = phoneMap.get(c.AccountId); then update.
Medium
Triggers
10. Stage Change Close Date Setter
Problem #78 · Salesforce Apex Coding Challenge
Problem Statement
Write a before update trigger on Opportunity that
automatically sets CloseDate to today when the StageName
changes to "Closed Won".
Use Trigger.oldMap to compare the old and new values — only update
records where the stage actually changed to avoid unnecessary writes.
Key Concept — Trigger.oldMap
Trigger.oldMap is a Map<Id, Opportunity> containing the
previous field values before the save. Access it with
Trigger.oldMap.get(opp.Id) to compare old vs new.
Constraints
- Trigger event: before update only
- Must use
Trigger.oldMap to detect the stage change
- Only set
CloseDate when stage transitions to Closed Won (not when it was already Closed Won)
- Handle bulk (up to 200 records) — no SOQL inside the loop
Example
Old StageName: Prospecting → New StageName: Closed Won → CloseDate = today ✓
Old StageName: Closed Won → New StageName: Closed Won → No change (already won)
Old StageName: Negotiation → New StageName: Needs Analysis → No change
Approach
- 1Use Trigger.oldMap.get(opp.Id) to get the old version of each record.
- 2Condition: opp.StageName == 'Closed Won' && oldOpp.StageName != 'Closed Won'
- 3This is a before trigger — assign opp.CloseDate = Date.today() directly without DML.
Medium
TriggersSOQL
11. Account Deletion Blocker
Problem #80 · Salesforce Apex Coding Challenge
Problem Statement
Write a before delete trigger on Account that
prevents deleting any Account that has related Contact records.
Call addError() on the record to surface a user-friendly message
and block the deletion.
Key Concept — Before Delete Trigger
In a before delete trigger:
Trigger.old contains the records about to be deleted
Trigger.new is always empty — use Trigger.old
- Calling
record.addError('message') blocks the deletion
Trigger.oldMap is a Map<Id, Account> for fast lookup
Constraints
- Collect all Account IDs first, then run one SOQL query (no SOQL in loop)
- Handle bulk: up to 200 accounts being deleted at once
- Gracefully skip accounts that have no contacts
Approach
- 1Query: SELECT AccountId FROM Contact WHERE AccountId IN :accountIds
- 2Build a Set of accountIds that have contacts from the query result.
- 3Call acc.addError('Cannot delete an Account with related Contacts.') to block deletion.
Hard
Triggers
12. Opportunity Undelete Restorer
Problem #81 · Salesforce Apex Coding Challenge
Problem Statement
Write an after undelete trigger on Opportunity that
reactivates opportunities when they are restored from the Recycle Bin.
For each undeleted opportunity with a StageName of "Closed Won"
or "Closed Lost", reset it to active status:
- Set
StageName to "Prospecting"
- Set
CloseDate to today + 30 days
Key Concept — After Undelete Trigger
after undelete is the only valid undelete event. In this context:
Trigger.new contains the restored records (with their Ids)
Trigger.old is null / empty — there is no "before" state for undelete
Trigger.isUndelete is true
- After triggers must use DML to persist field changes (you cannot assign fields directly)
Constraints
- Trigger event:
after undelete only
- Only modify opportunities that were Closed Won or Closed Lost
- Use a single DML update — collect into a list first
- Handle bulk: up to 200 records
Approach
- 1Check: opp.StageName == 'Closed Won' || opp.StageName == 'Closed Lost'
- 2In after triggers you cannot assign to Trigger.new records directly — create a new sObject: new Opportunity(Id=opp.Id, ...)
- 3Set CloseDate = Date.today().addDays(30) on the new Opportunity wrapper.
Medium
TriggersValidation
13. Deactivate Account Validation Trigger
Problem #135 · Salesforce Apex Coding Challenge
Problem Statement
Write a before update trigger on Account that prevents
an account from being deactivated (i.e. IsActive__c set to false)
if any linked Opportunity has a StageName that is
not Closed Won or Closed Lost.
If the condition is violated, throw a field error on IsActive__c
using addError().
Best Practices
- Separate trigger logic into a handler class (
AccountTriggerHandler).
- Use a single SOQL query — no SOQL inside loops.
- Bulkify: handle up to 200 records in one trigger invocation.
- Test class must cover positive, negative, and bulk scenarios.
Objects / Fields
Account — IsActive__c (Checkbox custom field)
Opportunity — AccountId, StageName
Approach
- 1In the handler, build a Set of Account Ids being deactivated (IsActive__c changed from true → false).
- 2Query Opportunities belonging to those accounts whose StageName is neither Closed Won nor Closed Lost.
- 3Build a Map (hasOpenOpps), then iterate Trigger.new to call acc.IsActive__c.addError(...).
- 4In your test, insert an Account with an open Opportunity and assert that the DML throws an exception.
Medium
TriggersValidation
14. Employee Termination Validation Trigger
Problem #136 · Salesforce Apex Coding Challenge
Problem Statement
Write a before update trigger on the custom object
Employee__c that blocks changing the Status__c field to
"Terminated" if there are no
Employee_Review__c records linked to that employee.
Throw a field error on Status__c via addError().
Best Practices
- Handler class pattern — keep trigger thin.
- Bulkify: one SOQL for all employees being terminated.
- Test: positive (has review → allowed), negative (no review → error), bulk.
Objects / Fields
Employee__c — Status__c (picklist: Active, On Leave, Terminated)
Employee_Review__c — Employee__c (Lookup to Employee__c)
Approach
- 1Collect Ids where Status__c is changing to "Terminated".
- 2Query Employee_Review__c once for all terminating Ids, grouping by the Employee__c lookup so you can count reviews per employee.
- 3Build a Set of employee Ids that HAVE reviews. Employees NOT in this set get addError().
- 4Test negative: insert Employee without reviews, try to set Status = Terminated, expect DmlException.
Hard
TriggersSOQL
15. Cross-Object IsActive Checkbox Sync
Problem #137 · Salesforce Apex Coding Challenge
Problem Statement
All three objects — Account, Contact, and User
— have a custom checkbox IsActive__c.
When Account.IsActive__c is unchecked (false):
- Set
IsActive__c = false on all related Contacts of that account.
- Set
IsActive__c = false on the User linked via
Account.OwnerId (only if that user's IsActive__c is currently true).
Best Practices
- Use after update trigger so updates to related records don't affect the in-flight Account.
- Single SOQL per object — no SOQL inside loops.
- Bulkify: multiple accounts can be deactivated at once.
- Test: deactivate account → assert contacts and owner user also deactivated.
Approach
- 1Collect all account Ids where IsActive__c changed true → false.
- 2One query for Contacts: find those belonging to the deactivated accounts that are still currently active.
- 3One query for Users: find the owner Users of those accounts that are still currently active.
- 4Update Contacts and Users in separate DML calls. Never mix SObject types in one list.
Medium
TriggersSOQLAggregate
16. Country-wise Account Count Tracker
Problem #139 · Salesforce Apex Coding Challenge
Problem Statement
Create a custom object Country_Account_Count__c with fields
Country__c (Text) and Account_Count__c (Number).
Write an after insert / after update / after delete / after undelete
trigger on Account that keeps this summary object up to date with the
total number of accounts per BillingCountry.
Best Practices
- Use a single SOQL aggregate query to recount after any change.
- Upsert (not insert) the summary records using an external Id or existing Ids.
- Handle blank BillingCountry gracefully — skip or group as "Unknown".
Approach
- 1After the trigger fires, collect affected BillingCountry values from both Trigger.new and Trigger.old.
- 2Run a single aggregate query on Account that counts records per BillingCountry, restricted to the affected countries.
- 3Query existing Country_Account_Count__c records by Country__c to upsert (not duplicate).
- 4Test: insert 3 US + 2 UK accounts, assert counts are correct in the summary object.
Medium
TriggersSOQL
17. Total Contact Count Rollup on Account
Problem #141 · Salesforce Apex Coding Challenge
Problem Statement
Create a custom field No_of_Total_Contact__c (Number) on the
Account object.
Write an after insert / after delete trigger on Contact
that updates No_of_Total_Contact__c on the parent account to always reflect
the live count of associated contacts.
Best Practices
- Collect all affected Account Ids first.
- One aggregate SOQL query grouped by Account to get each Account's live Contact count in a single round-trip.
- One DML: update all Account records in a single list.
- Test: insert 5 contacts → count = 5; delete 2 → count = 3.
Approach
- 1Use Trigger.isInsert ? Trigger.new : Trigger.old to get the right list.
- 2After deletion, the remaining contacts are still in the database — re-query the count.
- 3An account with no contacts left should have No_of_Total_Contact__c = 0.
- 4Test covers insert, delete, and contacts without AccountId (edge case).
Hard
TriggersSOQL
18. Sync Email Field Across Sibling Opportunities
Problem #142 · Salesforce Apex Coding Challenge
Problem Statement
An Account has 10 linked Opportunities.
There is a custom field Contact_Email__c (Email) on Opportunity.
When a user updates Contact_Email__c on any one of those
opportunities, automatically propagate that value to all other sibling
opportunities linked to the same account.
Best Practices
- Use after update — sibling updates must happen after the original is committed.
- Only propagate when
Contact_Email__c actually changed.
- Exclude the triggering opportunity itself to avoid a circular update.
- One SOQL, one DML — no queries inside loops.
Approach
- 1Build a Map of AccountId → new email for Opps where Contact_Email__c changed.
- 2Query the sibling Opportunities on those same accounts, excluding the opportunities that triggered the change.
- 3Update siblings where acc email differs from current value.
- 4Test: update email on one opp → assert all siblings updated; verify the triggering opp is NOT re-updated.
Hard
TriggersSOQLAutomation
19. Auto-Reassign Accounts & Opportunities on User Deactivation
Problem #143 · Salesforce Apex Coding Challenge
Problem Statement
When a Salesforce User is deactivated (IsActive set to
false), automatically reassign all of that user's Accounts
and Opportunities to the deactivated user's Manager
(User.ManagerId).
Best Practices
- Use before update — changing OwnerId on User is only possible
via System.runAs() or mixed DML workaround in tests.
- Use
@future method to update Account and Opportunity records
(mixed DML — you cannot update User and related non-setup objects in the
same transaction).
- One SOQL per object, one DML per object.
- Skip users with no ManagerId.
Approach
- 1Collect deactivating user Ids (IsActive true→false) and their ManagerId from Trigger.new.
- 2Pass a JSON string (or two Set params) to @future since you cannot pass SObjects to future methods.
- 3In the future method, query Accounts and Opportunities currently owned by any of the deactivated users.
- 4Test: create Manager + User, assign records, deactivate User, assert records moved to Manager.
Medium
TriggersApex ClassesBusiness Logic
20. Trigger: Set Closed-Won Date on Opportunity Stage Change
Problem #151 · Salesforce Apex Coding Challenge
Problem Statement
Your revenue operations team needs to know exactly when each opportunity
was first won. When an Opportunity's StageName is changed to
'Closed Won', automatically stamp the current date into a custom field
Closed_Won_Date__c (Date).
Requirements
- Use a before update trigger so the field is set before the record is saved.
- Only set
Closed_Won_Date__c when StageName changes
to 'Closed Won' (not if it was already 'Closed Won').
- Handle bulk updates — no SOQL or DML inside the trigger body.
- Delegate logic to a handler class
OpportunityTriggerHandler.
Best Practices
- One trigger per object — delegate everything to a handler class.
- Use
Trigger.oldMap to detect field changes.
- In tests, use
Test.startTest() / Test.stopTest() to isolate the trigger context.
Approach
- 1Compare opp.StageName == 'Closed Won' with oldOpp.StageName != 'Closed Won' to detect the transition.
- 2Set opp.Closed_Won_Date__c = Date.today() inside the handler — no DML needed (before trigger).
- 3Test the negative case: if StageName was already 'Closed Won', the date should NOT be re-stamped.
- 4Bulk test: insert 200 opps, update all to 'Closed Won', assert all have today's date.
Medium
TriggersSOQLRollup
21. Trigger: Rollup Contact Count onto Account
Problem #152 · Salesforce Apex Coding Challenge
Problem Statement
Your reporting team needs a real-time count of contacts per account stored in a
custom field Contact_Count__c (Number) on the Account object.
Write a trigger ContactTrigger on the Contact object that
keeps Account.Contact_Count__c accurate after inserts
and deletes.
Requirements
- Fire on
after insert and after delete
- Use a single aggregate SOQL query, grouped by Account, to get each affected Account's live Contact count
- Update
Account.Contact_Count__c for all affected accounts in one DML
- Delegate logic to
ContactTriggerHandler
Best Practices
- Collect all affected AccountIds before the query — no SOQL inside loops.
- Read the per-Account count off the AggregateResult and cast it to Integer.
- Test both insert and delete paths to verify the count increments and decrements.
Approach
- 1Collect AccountIds from the trigger list first, then run one aggregate SOQL grouped by AccountId.
- 2An AggregateResult row exposes your COUNT() alias via get('yourAlias') — cast it to Integer.
- 3Map each AccountId to its count, then set acc.Contact_Count__c from that map when building the update list.
- 4Test delete: insert 2 contacts, delete 1, then reload the Account and assert Contact_Count__c = 1.
Hard
TriggersApex ClassesAutomation
22. Trigger: Auto-Create Follow-Up Task for High Priority Cases
Problem #153 · Salesforce Apex Coding Challenge
Problem Statement
Your support team requires that whenever a High priority case is created,
a follow-up Task is automatically generated for the case owner reminding them
to respond within 2 hours.
Write a trigger CaseTrigger on the Case object and a handler
class CaseTriggerHandler that, on after insert:
- Identifies all newly inserted cases where
Priority = 'High'
- Creates one
Task per high-priority case with:
Subject = 'Follow-up: ' + case.CaseNumber
WhatId = case Id (links Task to the Case)
OwnerId = case OwnerId
ActivityDate = Date.today()
Priority = 'High'
Status = 'Not Started'
- Inserts all tasks in a single bulk DML
Best Practices
- Use after insert — Task.WhatId requires the Case to already have an Id.
- Filter for
Priority = 'High' before creating tasks — do not create tasks for Normal/Low cases.
- Collect all Task records first, then insert once — never DML inside a loop.
- Test that Normal priority cases do NOT generate tasks.
Approach
- 1Use after insert — you need the Case Id to populate Task.WhatId.
- 2Loop newCases, check c.Priority == 'High', build a Task and add to list.
- 3Task fields: Subject = 'Follow-up: ' + c.CaseNumber, WhatId = c.Id, OwnerId = c.OwnerId.
- 4Insert the task list once outside the loop — single DML for all tasks.
- 5Test the negative: insert a Normal case and assert [SELECT COUNT() FROM Task WHERE WhatId = :normalCase.Id] == 0.
Medium
Triggers
23. Trigger: Auto-Calculate Lead Score
Problem #156 · Salesforce Apex Coding Challenge
Problem Statement
Write a before insert / before update trigger on Lead
with handler LeadTriggerHandler that computes Lead_Score__c:
| Field | Condition | Points |
| LeadSource | Web | +10 |
| LeadSource | Phone Inquiry | +8 |
| LeadSource | Other | +5 |
| Title | Contains 'VP' or 'Director' | +15 |
| Title | Contains 'Manager' | +8 |
| Company | Not blank | +5 |
| Email | Not blank | +5 |
Scores are cumulative. Web + VP + Company + Email = 35 points.
Best Practices
- Reset score to 0 each iteration — idempotent on update.
- Use
String.isNotBlank() for null-safe string checks.
- Use
containsIgnoreCase() for Title matching.
Approach
- 1Start each loop iteration with: Integer score = 0;
- 2Use String.isNotBlank(lead.LeadSource) before comparing the value.
- 3For Title: lead.Title.containsIgnoreCase('VP') || lead.Title.containsIgnoreCase('Director')
- 4Test: LeadSource=Web + Title=VP + Company + Email should score exactly 35.
Medium
TriggersValidation
24. Trigger: Block Opportunity Stage Regression
Problem #157 · Salesforce Apex Coding Challenge
Problem Statement
Once an Opportunity reaches Closed Won or
Closed Lost, it must never be moved back to an open stage.
Write a before update trigger that delegates to
OpportunityTriggerHandler.preventStageRegression() and calls
addError('Cannot reopen a closed Opportunity.') when violated.
Best Practices
- Define a
Set<String> CLOSED_STAGES constant.
- Use
Trigger.oldMap to read the previous StageName.
- Only call addError when StageName actually changed to an open stage from a closed one.
Approach
- 1CLOSED_STAGES = new Set{'Closed Won', 'Closed Lost'}
- 2Detect regression: CLOSED_STAGES.contains(oldStage) && !CLOSED_STAGES.contains(newStage)
- 3Call opp.StageName.addError('Cannot reopen a closed Opportunity.')
- 4Test: insert Closed Won opp, update to Prospecting → expect DmlException.
Medium
TriggersValidation
25. Trigger: Prevent Duplicate Contact Email
Problem #158 · Salesforce Apex Coding Challenge
Problem Statement
Write a before insert / before update trigger on Contact
that prevents two contacts from sharing the same Email.
If a duplicate is found, call Email.addError('A Contact with this email already exists.')
Logic
- Collect non-blank emails from
Trigger.new.
- Run one SOQL: existing contacts with matching email, excluding records being updated
(
Id NOT IN :Trigger.newMap.keySet()).
- Build a
Set<String> of existing emails (lowercased).
- Loop and call
addError() on duplicates.
Best Practices
- One SOQL total — never inside a loop.
- Case-insensitive: use
.toLowerCase().
- Self-update safe: exclude the record being updated from SOQL.
Approach
- 1Build Set emailsToCheck from Trigger.new where Email is not blank.
- 2Query existing Contacts whose Email matches one of the incoming emails, then exclude the records currently being updated from that result.
- 3Lowercase both existing and incoming emails before comparing.
- 4Test: insert Contact A, insert Contact B with same email → DmlException.
Medium
Triggers
26. Trigger + Handler: Update Campaign Stats on Closed Won
Problem #159 · Salesforce Apex Coding Challenge
Problem Statement
When an Opportunity moves to Closed Won, increment
its linked Campaign.Won_Opportunities__c (Number) by 1.
Write OpportunityTrigger (after update) and
OpportunityTriggerHandler.
Logic
- Find opps where StageName changed to 'Closed Won' and
CampaignId != null.
- Collect unique CampaignIds.
- Query the affected Campaigns once, by their Ids, to read each one's current counter.
- Increment each campaign's counter and update in one DML.
Best Practices
- One SOQL, one DML — never inside loops.
- Compare old vs new StageName to avoid double-counting.
- Initialise
Won_Opportunities__c to 0 if null before incrementing.
Approach
- 1Detect change: opp.StageName == 'Closed Won' && oldMap.get(opp.Id).StageName != 'Closed Won'
- 2Query the Campaign fields you need to read and update, filtered to the collected CampaignIds.
- 3Null-safe increment: (camp.Won_Opportunities__c == null ? 0 : camp.Won_Opportunities__c) + 1
- 4Test: create Campaign → create Opp → update to Closed Won → assert Won_Opportunities__c == 1.
Hard
TriggersValidation
27. Trigger + Handler: Escalate Case Priority on SLA Breach
Problem #160 · Salesforce Apex Coding Challenge
Problem Statement
Write a before update trigger on Case with handler
CaseTriggerHandler. When a Case's Status is set to
'Escalated':
- Set
Priority to 'High'.
- Set
Escalation_Reason__c to 'SLA Breach — auto-escalated'.
- If the case was already
'Escalated' →
addError('Case is already escalated.').
Best Practices
- Use
Trigger.oldMap to compare old Status.
- No SOQL — all data in Trigger.new / oldMap.
- Test: valid escalation, double escalation, non-escalated change, bulk 50.
Approach
- 1Valid escalation: c.Status == 'Escalated' && oldMap.get(c.Id).Status != 'Escalated'
- 2Double escalation: oldMap.get(c.Id).Status == 'Escalated' → addError('Case is already escalated.')
- 3Set Priority = 'High' and Escalation_Reason__c in the valid branch.
- 4Bulk mixed: use Database.update(list, false) and check SaveResult[] for partial failures.
Medium
Triggers
28. Trigger + Handler: Auto-Assign Case Owner by Support Region
Problem #212 · Salesforce Apex Coding Challenge
Problem Statement
Write a before insert / before update trigger on Case
with handler CaseTriggerHandler that assigns the OwnerId to the
matching support queue based on a custom Region__c field.
Logic
- Collect unique
Region__c values from Trigger.new.
- Query the
Group object for queue records whose name matches one of the
collected regions.
- Build a
Map<String, Id> of region name → queue Id.
- Loop
Trigger.new and set c.OwnerId when a match exists.
Best Practices
- One SOQL for queues — never inside the loop.
- Guard with
regions.isEmpty() before the SOQL.
- Trigger delegates entirely to handler — zero business logic in the trigger file.
- Test covers null Region__c (graceful degradation) and the update path.
Approach
- 1Queues are stored as Group records — query them by their queue-type record designation and match their Name against the set of collected regions.
- 2Build map: Map regionToQueueId = new Map(); for (Group g : ...) regionToQueueId.put(g.Name, g.Id);
- 3Assign: if (regionToQueueId.containsKey(c.Region__c)) c.OwnerId = regionToQueueId.get(c.Region__c);
- 4Trigger should only contain: CaseTriggerHandler.assignOwnerByRegion(Trigger.new);
Medium
TriggersValidation
29. Trigger + Handler: Block Account Deletion When Open Opportunities Exist
Problem #213 · Salesforce Apex Coding Challenge
Problem Statement
Write a before delete trigger on Account that prevents
deletion when there are open (non-closed) Opportunities linked to the Account.
Logic
- Collect all Account Ids from
Trigger.old.
- Query for open (non-closed) Opportunities linked to those Accounts in a single SOQL call.
- Build a
Map<Id, List<Opportunity>> grouped by AccountId.
- Call
acc.addError('Cannot delete Account: N open Opportunity(ies)...') for each match.
Best Practices
- One SOQL — never inside a loop.
- Message includes the count of blocking records for user clarity.
- Test covers 3 scenarios: blocked (open opp), allowed (no opp), allowed (closed opp only).
Approach
- 1Before delete: use Trigger.old (Trigger.new is not available for delete triggers).
- 2Query Opportunity records linked to the collected Account Ids, filtered to only the ones still open.
- 3addError: acc.addError('Cannot delete Account: open opportunities exist.');
- 4Test the blocked path by expecting DmlException in a try-catch block.
Hard
Triggers
30. Trigger + Handler: Rollup Closed-Won Revenue onto Account
Problem #214 · Salesforce Apex Coding Challenge
Problem Statement
Write OpportunityRevenueRollupTrigger (after insert/update/delete) and
OpportunityRevenueRollupHandler that maintains
Total_Closed_Won_Revenue__c on Account — the sum of Amount for all
Closed Won Opportunities.
Logic
- Trigger collects affected Account Ids (from Trigger.new AND Trigger.old for updates,
Trigger.old for deletes).
- Handler uses one
AggregateResult SOQL with SUM(Amount) GROUP BY AccountId.
- Builds Map of AccountId → total revenue, queries Accounts, updates only changed records.
Best Practices
- After update: include both old and new AccountIds — the opp may have been
moved to a different account.
- Use
AggregateResult — never sum in a loop.
- Skip records where the value has not changed.
- Test covers: insert (revenue increases), delete (revenue decreases), open opp (not counted).
Approach
- 1Trigger: for delete, use Trigger.old; for insert/update use Trigger.new. For updates also include Trigger.old AccountIds.
- 2An AggregateResult query grouped by AccountId, filtered to closed-won opportunities, gets you each Account's total in one round-trip.
- 3Read aggregate value: (Decimal) ar.get('yourAliasName')
- 4Zero-revenue accounts: use revenueByAccount.containsKey(acc.Id) ? ... : 0 to handle accounts with no won opps.
Medium
Triggers
31. Apex Trigger: Auto-Set Opportunity Probability on Stage Change
Problem #218 · Salesforce Apex Coding Challenge
Problem Statement
Write a before insert / before update trigger on Opportunity
that automatically sets Probability based on StageName.
Stage → Probability Mapping
| Stage | Probability |
| Prospecting | 10 |
| Qualification | 20 |
| Needs Analysis | 30 |
| Value Proposition | 40 |
| Id. Decision Makers | 50 |
| Perception Analysis | 60 |
| Proposal/Price Quote | 70 |
| Negotiation/Review | 80 |
| Closed Won | 100 |
| Closed Lost | 0 |
Best Practices
- Store the mapping as a
final Map<String, Integer> constant.
- On updates: skip records where
StageName has not changed to avoid
overwriting manual overrides.
- Use
Trigger.oldMap to detect stage transitions.
- Check
containsKey() before calling get().
- No SOQL or DML needed — pure field assignment.
Approach
- 1Declare: final Map STAGE_PROBABILITY = new Map{ 'Prospecting' => 10, ... };
- 2Skip unchanged stages on update: if (Trigger.isUpdate && opp.StageName == Trigger.oldMap.get(opp.Id).StageName) continue;
- 3Safe lookup: if (STAGE_PROBABILITY.containsKey(opp.StageName)) opp.Probability = STAGE_PROBABILITY.get(opp.StageName);
- 4Trigger.oldMap is only available on update and delete events — guard with Trigger.isUpdate.
Medium
TriggersValidation
32. Trigger + Handler: Prevent Duplicate Lead Email on Insert and Update
Problem #227 · Salesforce Apex Coding Challenge
Problem Statement
Write a before insert / before update trigger on Lead
with handler LeadDuplicateEmailHandler that prevents two unconverted Leads
from sharing the same email address.
Logic
- Skip blank emails and unchanged emails on update.
- Query existing unconverted Leads with matching emails, excluding the records
being updated (
Id NOT IN :currentIds).
- Call
l.Email.addError('A Lead with this email address already exists.')
on duplicates.
Best Practices
- Normalise to lowercase before comparing emails.
- Exclude the current record(s) from the duplicate query on updates.
- Use
String.isBlank() — handles null AND empty strings.
- One SOQL — never inside a loop.
Approach
- 1Normalise: emailsToCheck.add(l.Email.toLowerCase()) — avoids case-sensitivity false positives.
- 2Exclude self: AND Id NOT IN :currentIds — where currentIds = oldMap != null ? oldMap.keySet() : new Set()
- 3Field error: l.Email.addError('message') — highlights the Email field in the UI.
- 4Test: update same email should NOT error — verify with an update-same-email test case.
Medium
Triggers
33. Apex Trigger: Auto-Populate Contact Description from Account on Insert
Problem #231 · Salesforce Apex Coding Challenge
Problem Statement
Write a before insert trigger on Contact that copies
the parent Account's Description into the Contact's Description
field — but only when the Contact's Description is blank.
Requirements
- Fires on
before insert only.
- Collect Account Ids from Contacts that have an AccountId.
- Guard empty set before the SOQL query.
- Query Accounts (
Id, Description) in one SOQL.
- Build a
Map<Id, Account>.
- In second loop: skip Contacts with no AccountId; skip if Contact already has a
Description; copy Account Description.
Best Practices
- Two loops — collect then apply — keeps SOQL outside the loop.
- Never overwrite a manually set Contact Description.
- Guard with
isEmpty() before the SOQL.
Approach
- 1Collect: for (Contact c : Trigger.new) { if (c.AccountId != null) accountIds.add(c.AccountId); }
- 2Map: build a Map from a single query of the collected Account Ids (Id and Description fields) so each Contact can look up its parent Account in O(1).
- 3Skip existing: if (c.Description != null) continue;
- 4Copy: c.Description = acc.Description; — this works in before insert because the record hasn't been committed yet.
Medium
Apex Trigger
34. Trigger + Handler: Auto-Format Contact Phone on Insert/Update
Problem #285 · Salesforce Apex Coding Challenge
Problem Statement
Implement a Trigger + Handler that auto-formats the
Phone field on Contact to US format
(XXX) XXX-XXXX whenever a 10-digit number is entered.
Trigger Requirements
- Fire on before insert and before update
- Contains no business logic — delegates entirely to handler
Handler Requirements
- Skip records where
Phone has not changed on update (use oldMap)
- Strip all non-digit characters, then format if exactly 10 digits
- If not 10 digits or blank, leave the value unchanged
- Mark the private format helper
@TestVisible
Test Class Requirements
- Cover: insert formats phone, update skips unchanged phone, invalid phone unchanged, null phone skipped
- Use
Test.startTest()/stopTest()
Approach
- 1In the trigger, call only one line: ContactPhoneHandler.formatPhones(Trigger.new, Trigger.oldMap).
- 2In the handler loop, check oldMap != null && old.Phone == con.Phone before skipping.
- 3Use replaceAll('[^0-9]', '') to strip non-digit characters.
- 4Mark the private helper @TestVisible so your test class can call it directly.
Medium
Apex Trigger
35. Trigger + Handler: Block Opportunity Close When Amount Is Null
Problem #286 · Salesforce Apex Coding Challenge
Problem Statement
Implement a Trigger + Handler that prevents an
Opportunity from being set to a closed stage
(Closed Won or Closed Lost) when
Amount is null.
Trigger Requirements
- Fire on before insert and before update
- Zero logic in the trigger body — one call to the handler
Handler Requirements
- Define closed stages in a
Set constant
- Skip records that are not in a closed stage
- Skip records where Amount is not null
- Call
opp.Amount.addError() with a clear message
Test Class Requirements
- Assert DmlException thrown for Closed Won with null Amount
- Assert open-stage Opportunity with null Amount saves successfully
- Assert Closed Won with a non-null Amount saves successfully
Approach
- 1Store closed stages in a static Set constant for O(1) lookups.
- 2Use CLOSED_STAGES.contains(opp.StageName) to check if the stage is closed.
- 3Call addError on the specific field: opp.Amount.addError('...') for a field-level error.
- 4In the test, catch DmlException and assert on e.getMessage().
Hard
Apex Trigger
36. Trigger + Handler: Rollup Open Opportunity Count onto Account
Problem #288 · Salesforce Apex Coding Challenge
Problem Statement
Implement a Trigger + Handler that maintains an
Open_Opportunity_Count__c field on Account
reflecting the number of open (non-closed) Opportunity records
linked to each account.
Trigger Requirements
- Fire on after insert, after update, after delete, after undelete
- Use separate method calls per event —
if (Trigger.isInsert), if (Trigger.isUpdate), etc.
- Pass
Trigger.new / Trigger.old and Trigger.oldMap — never pass Trigger.isDelete as a parameter
Handler Requirements
- One public method per event (
onAfterInsert, onAfterUpdate, onAfterDelete, onAfterUndelete)
- Each public method delegates to a private
updateCounts method
- Collect all affected Account IDs from both the records list and
oldMap (handles reparenting)
- Query open opportunities with
IsClosed = false grouped by AccountId using AggregateResult
- Initialise all accounts to 0 in the map before applying query results (handles deletions correctly)
- Single SOQL query — bulk-safe for 200+ records
Test Class Requirements
- Assert count increments on opportunity insert
- Assert count decrements on opportunity delete
Approach
- 1Use separate handler methods per event (onAfterInsert, onAfterUpdate, etc.) — this makes each method independently testable without a trigger context.
- 2Initialise countMap with 0 for every affected account before the query — this ensures deleted opps result in 0, not a missing key.
- 3Use AggregateResult with COUNT(Id) and GROUP BY AccountId in a single SOQL.
- 4In onAfterUpdate, pass oldMap so updateCounts can collect AccountIds from both new and old records (handles reparenting).
Medium
Apex Trigger
37. Trigger + Handler: Block Contact Deletion When Linked to Closed Won Opportunity
Problem #289 · Salesforce Apex Coding Challenge
Problem Statement
Implement a Trigger + Handler that prevents deletion of
a Contact if that contact is linked (via
OpportunityContactRole) to a Closed Won
Opportunity.
Trigger Requirements
- Fire on before delete only
- Pass
Trigger.old to the handler
Handler Requirements
- Collect all Contact IDs being deleted into a
Set
- Single SOQL on
OpportunityContactRole filtering
ContactId IN :contactIds and
Opportunity.StageName = 'Closed Won'
- Call
con.addError() with a clear message for blocked contacts
Approach
- 1Query OpportunityContactRole and filter on Opportunity.StageName using a cross-object filter.
- 2Build a Set of blocked contact IDs from the query result, then iterate oldList to call addError.
- 3One SOQL is enough — no need to query Opportunity separately.
- 4In the test, create an OpportunityContactRole to link the contact to the opportunity.
Medium
Triggers
38. Trigger: Maintain Contact Count Rollup on Account
Problem #326 · Salesforce Apex Coding Challenge
Problem Statement
Write an Apex trigger UpdateContactCount on the Contact
object that fires on after insert, after delete, and
after undelete.
After each event, recalculate the number of Contacts for each affected Account and
store the result in a custom field Total_Contacts__c on the Account.
Requirements
- Use a single aggregate SOQL query — no queries inside loops.
- Handle the case where an Account's last Contact is deleted (count → 0).
- Collect Account IDs from
Trigger.new on insert/undelete and from
Trigger.old on delete.
Approach
- 1Use
Trigger.isDelete ? Trigger.old : Trigger.new to pick the right list. - 2Run one aggregate query on Contact that counts related records per Account for the whole batch of accountIds.
- 3Store results in
Map<Id, Integer>. If an account has no rows in the result, its count is 0. - 4Build
new Account(Id = accId, Total_Contacts__c = count) and call a single update.
Medium
Triggers
39. Trigger: Enforce Unique Email Across All Leads
Problem #327 · Salesforce Apex Coding Challenge
Problem Statement
Write an Apex trigger LeadUniqueEmailCheck on the Lead
object that fires before insert and before update.
If any incoming Lead has an Email that already exists on another Lead record
(case-insensitive comparison), block the save with:
"Email must be unique across all Leads."
Requirements
- Case-insensitive comparison (use
.toLowerCase()).
- On update, do not flag a Lead against itself.
- Bulk-safe: one SOQL query for all emails in the batch.
Approach
- 1Normalise emails with
.toLowerCase() when building the set and when comparing. - 2Query existing Leads whose Email matches anything in the incoming batch's normalised email set, in one bulk call.
- 3Skip the record itself on update:
existingLead.Id != newLead.Id. - 4Call
newLead.addError('Email must be unique across all Leads.').
Medium
Triggers
40. Trigger: Cascade Account Billing Address to Related Contacts
Problem #328 · Salesforce Apex Coding Challenge
Problem Statement
Write an Apex trigger UpdateContactBillingAddress on Account
that fires after update.
Whenever any billing address field changes on an Account, copy the new billing
address to the mailing address of all related Contact records.
Address Fields to Sync
- Account:
BillingStreet, BillingCity, BillingState, BillingPostalCode, BillingCountry
- Contact:
MailingStreet, MailingCity, MailingState, MailingPostalCode, MailingCountry
Requirements
- Only update Contacts when at least one billing field actually changed.
- Use
Trigger.oldMap to detect changes.
- Bulk-safe — one SOQL and one DML for all affected Accounts.
Approach
- 1Compare each billing field with its old value:
acc.BillingStreet != old.BillingStreet. - 2Collect changed Account IDs into a
Set<Id> then query all their Contacts once. - 3Build a
Map<Id, Account> from Trigger.newMap so you can look up the new address per Contact. - 4Assign all five mailing fields on each Contact before adding to the update list.
Hard
Triggers
41. Trigger: Rollup Sum from Child Custom Object to Parent
Problem #329 · Salesforce Apex Coding Challenge
Problem Statement
Write an Apex trigger CalculateTotalOnCustomObjectA on
CustomObjectB__c that fires on after insert,
after update, and after delete.
Each CustomObjectB__c record has a lookup to CustomObjectA__c
(field: CustomObjectA__c) and a currency field Value__c.
After every change, recalculate and store the sum of all Value__c
for each parent in the Total_Value__c field on CustomObjectA__c.
Requirements
- Use an aggregate SOQL with
SUM() and GROUP BY.
- Handle delete: use
Trigger.old to collect parent IDs.
- Single SOQL and single DML.
Approach
- 1Use
Trigger.isDelete ? Trigger.old : Trigger.new to pick the right list. - 2Run a single aggregate query on CustomObjectB__c that sums Value__c per parent Id for the whole batch of parentIds.
- 3Cast the result:
(Id)r.get('CustomObjectA__c') and (Decimal)r.get('totalValue'). - 4Build
new CustomObjectA__c(Id = parentId, Total_Value__c = sum) and update in one DML call.
Medium
Triggers
42. Trigger: Flag Out_of_Zip__c When Contact Postal Code Differs
Problem #331 · Salesforce Apex Coding Challenge
Problem Statement
Write an Apex trigger FlagOutOfZip on Account that
fires before update.
When an Account's BillingPostalCode changes, check all related Contacts.
If any Contact's MailingPostalCode does not match the Account's
(new) BillingPostalCode, set Out_of_Zip__c = true
on the Account; otherwise set it to false.
Requirements
- Only process Accounts where
BillingPostalCode actually changed.
- Bulk-safe: one SOQL query for all affected Accounts.
- This is a before trigger — mutate
acc.Out_of_Zip__c directly.
Approach
- 1Compare
acc.BillingPostalCode != Trigger.oldMap.get(acc.Id).BillingPostalCode. - 2Query the related Contacts for the batch of changedIds in one call, traversing the parent-to-child relationship to also read the Account's (new) BillingPostalCode alongside each Contact's MailingPostalCode.
- 3Build a
Map<Id, Boolean> — true if any Contact postal code differs. - 4Since this is a before trigger, update
acc.Out_of_Zip__c directly in the Trigger.new loop — no DML needed.
Medium
Triggers
43. Trigger: Auto-Create Parent Account for Orphaned Contacts
Problem #333 · Salesforce Apex Coding Challenge
Problem Statement
Write an Apex trigger CreateAccountForOrphanContact on Contact
that fires before insert.
If a Contact is being inserted with no AccountId, automatically create a
parent Account using the Contact's LastName as the Account name
and Phone as the Account phone, then assign the new Account's Id back to the
Contact's AccountId.
Requirements
- A before-insert trigger allows direct field assignment — no Contact update DML needed.
- Create all Accounts in one bulk DML insert.
- Maintain order to correctly map each orphan Contact to its new Account.
Approach
- 1Build
new Account(Name = con.LastName, Phone = con.Phone) and add to newAccounts. - 2Track the orphan Contact in a parallel
orphans list to keep index alignment. - 3After insert, loop with index:
orphans[i].AccountId = newAccounts[i].Id. - 4Because this is a before-insert trigger, assigning
AccountId on the Contact in Trigger.new is sufficient — no extra DML.
Hard
Triggers
44. Trigger: Flag needintel__c When 70%+ Contacts Are Inactive
Problem #334 · Salesforce Apex Coding Challenge
Problem Statement
Write a trigger UpdateContact on Contact (after update) and
a handler class ContactHandler with a static method
checkDeadIntel(List<Contact>).
When Contacts are updated, check each related Account: if 70% or more
of its Contacts have Dead__c = true, set needintel__c = true on
that Account.
Requirements
- Use two separate aggregate queries — one for total count, one for dead count.
- Work out each Account's dead-Contact percentage from those two counts, guarding against division by zero.
- Bulk-safe: handle multiple Contacts from multiple Accounts.
- Single Account update DML.
- Write a test class with at least two test methods verifying both cases.
Approach
- 1Run an aggregate query on Contact that counts every related record per Account for the whole batch of accIds — this gives you the total count.
- 2Run a second aggregate query the same way, this time counting only the Contacts on each Account where Dead__c is true.
- 3Guard against dividing by zero, then work out each Account's dead percentage with integer-safe math and flag it once that percentage reaches the 70% threshold from the requirements.
- 4Build
new Account(Id = accId, needintel__c = true) and update in a single DML call. - 5In the test class use
Test.startTest() / Test.stopTest() around the update DML to trigger async governor-limit reset.
Medium
Triggers
45. Trigger: Update Contact Profile URL When Account Website Changes
Problem #336 · Salesforce Apex Coding Challenge
Problem Statement
Write an Apex trigger UpdateContactProfile on Account
that fires after update.
When an Account's Website field changes, update the Profile__c
field on all related Contacts using this format:
Profile__c = Website + "/" + FirstInitial + LastName
Example: Website = https://acme.com, Contact = John Smith
→ Profile__c = "https://acme.com/JSmith"
Requirements
- Only process Accounts where
Website actually changed and is not null.
- Bulk-safe — one SOQL, one DML.
- Skip Contacts with a null
FirstName.
Approach
- 1Compare each Account's new Website to its prior value from
oldMap, and only treat it as changed when the new value is also non-null. - 2The SOQL already retrieves
Account.Website — use con.Account.Website in the loop. - 3Build the Profile__c value by combining the Account's Website, a "/", the Contact's first-name initial, and their LastName — matching the format shown in the problem statement.
- 4Skip nulls with
if (con.FirstName == null) continue;.
Medium
Triggers
46. Trigger: Flag is_gold__c on Accounts with Opportunities Over $20,000
Problem #337 · Salesforce Apex Coding Challenge
Problem Statement
Write an Apex trigger MarkGoldAccount on Opportunity
that fires on after insert, after update, and
after delete.
After each event, check each related Account: if any of its Opportunities has
Amount > 20000, set is_gold__c = true on the Account;
otherwise set it to false.
Requirements
- Use a single aggregate SOQL with
COUNT() and a HAVING
clause — or a regular query with Amount > 20000.
- Handle the delete case: use
Trigger.old to collect Account IDs.
- Bulk-safe: one SOQL and one DML for all affected Accounts.
Approach
- 1Query the Opportunities for the batch of accountIds, filtered down to just the high-value ones from the requirements, and pull back only the AccountId.
- 2Collect the result AccountIds into a
Set<Id> called goldAccIds. - 3For every accountId:
is_gold__c = goldAccIds.contains(accId) — this sets it true OR false correctly. - 4Update all Accounts in a single DML call.
Medium
TriggersAutomation
47. Copy Account Billing Address to New Contact
Problem #370 · Salesforce Apex Coding Challenge
Problem Statement
Write a before insert trigger on the standard object Contact
that copies the parent Account's Billing address into the Contact's Mailing
address fields, but only when the Contact's own mailing address is blank.
Best Practices
- Collect the relevant
AccountIds into a Set first.
- Query all needed Accounts in a single SOQL call, then loop the Contacts again to apply the fields.
Objects / Fields
Contact — AccountId, MailingStreet/City/State/PostalCode/Country
Account — BillingStreet/City/State/PostalCode/Country
Approach
- 1Only collect AccountIds for Contacts where MailingStreet AND MailingCity are both null.
- 2Query the related Accounts (with their Billing address fields) in a single batched call keyed by Account Id.
- 3Build a Map so the second loop can look up each Contact's Account in O(1).
- 4Test: a blank-address Contact (gets copied), a Contact with its own address (unchanged), and a bulk mix.
Medium
TriggersAutomation
48. Auto-Create Follow-Up Task for High-Priority Cases
Problem #371 · Salesforce Apex Coding Challenge
Problem Statement
Write an after insert trigger on the standard object Case
that automatically creates a follow-up Task for the Case owner whenever a new
Case is logged with Priority = 'High'.
Best Practices
- Build the full list of Tasks first, then insert once — never insert inside the loop.
- after insert is required since the Case needs an Id to link the Task via
WhatId.
Objects / Fields
Case — Priority, OwnerId, CaseNumber
Task — WhatId, OwnerId, Subject, ActivityDate, Status, Priority
Approach
- 1Loop Trigger.new; check c.Priority == 'High' and c.OwnerId != null.
- 2Build each Task with WhatId = c.Id, OwnerId = c.OwnerId, and a Subject referencing c.CaseNumber.
- 3Insert the full Task list once, after the loop.
- 4Test: a High priority Case (creates 1 Task), a Medium priority Case (creates 0), and a bulk mix.
Medium
TriggersRollup
49. Cascade Account Status Change to Related Contacts
Problem #372 · Salesforce Apex Coding Challenge
Problem Statement
Write an after update trigger on the standard object Account
that cascades a custom field Account_Status__c to all related Contact
records whenever it changes on the Account.
Best Practices
- Only recalculate for Accounts whose
Account_Status__c actually changed
(compare against Trigger.oldMap) — don't cascade on every unrelated edit.
- Query related Contacts with a single SOQL call, update once.
Objects / Fields
Account — Account_Status__c (custom Text/Picklist)
Contact — AccountId, Account_Status__c (custom)
Approach
- 1Build a Set of Account Ids where acc.Account_Status__c != oldMap.get(acc.Id).Account_Status__c.
- 2Also build a Map of the new status keyed by AccountId for the second pass.
- 3Query the related Contacts for those changed Accounts in one call, set each Contact's Account_Status__c from the map, then update once.
- 4Test: change the status on an Account with 2 Contacts and assert both Contacts are updated.
Medium
TriggersValidation
50. Block Contact Deletion When Linked to an Open Opportunity
Problem #373 · Salesforce Apex Coding Challenge
Problem Statement
Write a before delete trigger on the standard object Contact
that blocks deletion if the Contact is linked to any Opportunity (via
OpportunityContactRole) whose StageName is not 'Closed Lost'.
Best Practices
- Use the standard
OpportunityContactRole junction — Contact has no direct
Opportunity lookup out of the box.
- One SOQL query for all Contacts being deleted, not per record.
Objects / Fields
Contact, OpportunityContactRole.ContactId, Opportunity.StageName
Approach
- 1Collect Contact Ids from Trigger.old (or the oldList passed to the handler).
- 2Query OpportunityContactRole for those Contact Ids, filtering to roles whose related Opportunity is not in a closed-lost stage.
- 3Collect the matching ContactIds into a Set, then loop the old Contacts and addError() on any match.
- 4Test: a Contact tied to an open Opportunity (blocked) and one tied only to a Closed Lost Opportunity (allowed).
Medium
TriggersAutomation
51. Update Last Activity Date on Account/Contact From Task
Problem #374 · Salesforce Apex Coding Challenge
Problem Statement
Write an after insert trigger on the standard object Task
that updates a custom field Last_Activity_Date__c on the related
Account (via WhatId) or Contact (via WhoId)
to today's date.
Best Practices
WhatId/WhoId are polymorphic — use getSObjectType()
to check which object each Id belongs to before querying/updating.
- Combine Account and Contact updates into a single mixed
List<SObject>
so one update call handles both.
Objects / Fields
Task — WhatId, WhoId
Account.Last_Activity_Date__c, Contact.Last_Activity_Date__c (both custom Date)
Approach
- 1Loop the new Tasks; if t.WhatId != null and its type is Account, put Date.today() into an accountUpdates map keyed by Id.
- 2Do the same for t.WhoId when its type is Contact.
- 3Build one List combining `new Account(Id=.., Last_Activity_Date__c=..)` and `new Contact(Id=.., Last_Activity_Date__c=..)` entries.
- 4Test: a Task with WhatId on an Account, and a separate Task with WhoId on a Contact.
Medium
TriggersValidation
52. Block Project Completion While Child Tasks Are Open
Problem #375 · Salesforce Apex Coding Challenge
Problem Statement
Write a before update trigger on the custom object Project__c
that prevents changing Status__c to "Completed" if there are related
Task__c records still marked "Open".
Best Practices
- Only check Projects actually transitioning into Completed (compare against
oldMap).
- One SOQL query for all such Projects, not per record.
Objects / Fields
Project__c — Status__c
Task__c — Project__c (lookup), Status__c
Approach
- 1Collect a Set of Projects where Status__c is becoming Completed (new == Completed, old != Completed).
- 2Query the child Task__c records for those transitioning Projects, filtered down to the ones still marked Open.
- 3Collect the matching Project Ids into a Set, then loop newList and addError() on any match.
- 4Test: a Project with an open Task (blocked) and the same Project after the Task is closed (allowed).
Medium
TriggersValidation
53. Validate Timesheet Weekly Hours Do Not Exceed 40
Problem #376 · Salesforce Apex Coding Challenge
Problem Statement
Write a before insert trigger on the custom object Timesheet__c
that prevents saving a new Timesheet if the employee's total hours for that week (existing
records + the new batch) would exceed 40.
Best Practices
- Use an aggregate SOQL grouped by Employee and week to get existing totals in one query.
- Track a running total per employee/week key so multiple new Timesheet rows for the
same employee/week arriving in the same bulk batch are correctly summed together.
Objects / Fields
Timesheet__c — Employee__c (lookup), Week_Start_Date__c (Date), Hours__c (Number)
Approach
- 1Run a single aggregate query that sums Hours__c for the relevant employees, grouped by employee and week start date.
- 2Build a key like employeeId + '_' + weekStart to index both the existing totals map and a running-total map for the current batch.
- 3For each new Timesheet, add its Hours__c to the running total for its key; if the result exceeds 40, addError().
- 4Test: a single new Timesheet that alone exceeds 40 (blocked), and two smaller new Timesheets in the same batch for the same employee/week whose combined total exceeds 40 (also blocked).
Medium
TriggersValidation
54. Block Vendor Deletion While Active Purchase Orders Exist
Problem #377 · Salesforce Apex Coding Challenge
Problem Statement
Write a before delete trigger on the custom object Vendor__c
that prevents deletion if there are related Purchase_Order__c records with
Status__c = 'Active'.
Best Practices
- One SOQL query for all Vendors being deleted — never per record.
Objects / Fields
Vendor__c, Purchase_Order__c.Vendor__c (lookup), Purchase_Order__c.Status__c
Approach
- 1Collect Vendor Ids from the oldList passed to the handler.
- 2Query Purchase_Order__c for those Vendor Ids, filtered down to the Active-status records only.
- 3Collect the matching Vendor Ids into a Set, then loop oldVendors and addError() on any match.
- 4Test: a Vendor with an Active PO (blocked) and one with only Closed POs (allowed).
Hard
TriggersRollup
55. Roll Up Open Opportunity Amount Onto the Account
Problem #378 · Salesforce Apex Coding Challenge
Problem Statement
Write an after insert, after update trigger on the standard object
Opportunity that rolls up the total Amount of all
open (IsClosed = false) Opportunities onto a custom field
Open_Opportunity_Total__c on the parent Account.
Best Practices
- Only recalculate Accounts whose Opportunities actually changed
Amount or
StageName — not on every unrelated field edit.
- Use an aggregate
SUM(...) query grouped by AccountId — one query regardless
of batch size.
- Dedup target Accounts with a
Set<Id> so each Account is updated exactly once.
Objects / Fields
Opportunity — AccountId, Amount, StageName, IsClosed
Account.Open_Opportunity_Total__c (custom Currency)
Approach
- 1On insert there is no oldMap — always recalc. On update, only recalc if Amount or StageName changed.
- 2Collect the affected AccountIds into a Set.
- 3Run a single aggregate query summing Amount for open Opportunities on those Accounts, grouped by AccountId.
- 4Build one Account update per Id in the set (default to 0 if the map has no entry for it), then update once.
Hard
TriggersAutomation
56. Auto-Escalate Cases Older Than 5 Days
Problem #379 · Salesforce Apex Coding Challenge
Problem Statement
Write a before update trigger on the standard object Case that
sets Status to "Escalated" whenever a Case is not already
Closed or Escalated, and its CreatedDate is more than
5 days old.
Important Caveat
A trigger only fires on DML. A Case nobody touches for 5 days will never
re-enter this trigger on its own. In production, pair this logic with a nightly
Scheduled/Batch Apex job that queries stale open Cases and updates them
(which then safely re-fires this same trigger). This exercise focuses on the escalation
logic itself, assuming the Case is being updated.
Objects / Fields
Case — Status, CreatedDate (standard)
Approach
- 1Use Test.setCreatedDate() in your test to backdate a Case's CreatedDate for a deterministic test.
- 2Compute age with c.CreatedDate.date().daysBetween(Date.today()) > 5.
- 3Skip Cases already Status == 'Closed' or Status == 'Escalated'.
- 4Set c.Status = 'Escalated' directly — no addError needed, this is a corrective update, not a validation.
Hard
TriggersRollup
57. Roll Up Expense Amount to Parent Project (Full DML Coverage)
Problem #380 · Salesforce Apex Coding Challenge
Problem Statement
Write an after insert, after update, after delete trigger on the custom
object Expense__c that rolls up the total Amount__c of all child
Expense records onto a custom field Total_Expense__c on the parent
Project__c.
Best Practices
- A rollup that only handles insert/update will drift out of sync the moment someone
deletes an Expense — cover all three DML contexts.
- Use one aggregate
SUM(...) query per context, grouped by Project__c.
Objects / Fields
Expense__c — Project__c (lookup), Amount__c (Currency)
Project__c.Total_Expense__c (custom Currency)
Approach
- 1Write a shared private helper rollupToProjects(Set projectIds) used by all three handler methods.
- 2For insert/update, collect Project__c ids from the new records. For delete, collect them from the old records.
- 3Run one aggregate query summing Amount__c for those Projects, grouped by Project__c.
- 4Build one Project__c update per Id in projectIds, defaulting Total_Expense__c to 0 if the map has no entry — this correctly zeroes out a Project whose last Expense was just deleted.
Hard
TriggersConcurrency
58. Decrement Product Stock When an Order Item Is Created
Problem #381 · Salesforce Apex Coding Challenge
Problem Statement
Write an after insert trigger on the custom object Order_Item__c
that decrements Quantity_In_Stock__c on the related Product__c,
blocking the Order Item with addError() if there isn't enough stock.
Best Practices
- Query the related Products with
FOR UPDATE to lock those rows for the
duration of the transaction, preventing two simultaneous orders from over-selling the same stock.
- Bulk-safe: one query for all Products referenced in the batch.
Objects / Fields
Order_Item__c — Product__c (lookup), Quantity__c (Number)
Product__c.Quantity_In_Stock__c (Number)
Approach
- 1Collect Product Ids from the Order Items into a Set.
- 2Query the referenced Products (locking the rows for update) keyed by Id into a Map.
- 3For each Order Item, compute current stock minus Quantity__c; if negative, addError() on the Order Item, otherwise update the in-memory Product.
- 4Update the Product map's values once after the loop.
Hard
TriggersAsync
59. Email the Owner When a Contract Expires
Problem #382 · Salesforce Apex Coding Challenge
Problem Statement
Write an after update trigger on the custom object Contract__c
that sends an email alert to the record Owner whenever
Contract_Status__c transitions to "Expired".
Best Practices
- Email sending (like any slow, callout-adjacent operation) should not run synchronously
inside a trigger's transaction — dispatch it via an
@future method.
- Only enqueue the future call for Contracts that actually transitioned into Expired.
Objects / Fields
Contract__c — Contract_Status__c, OwnerId (standard)
Approach
- 1Compare ct.Contract_Status__c to oldMap.get(ct.Id).Contract_Status__c to detect the transition into Expired.
- 2Collect qualifying OwnerIds into a Set and pass that Set into an @future method — @future methods only accept primitive/collection-of-primitive parameters.
- 3Inside the future method, query User emails and send one Messaging.SingleEmailMessage per owner.
- 4In your test, use Test.startTest()/Test.stopTest() and assert Limits.getFutureCalls() to verify the async call was (or was not) enqueued — that is a reliable, deterministic assertion for future methods.
Hard
TriggersAutomation
60. Round-Robin Assign New Tickets to Support Agents
Problem #383 · Salesforce Apex Coding Challenge
Problem Statement
Write an after insert trigger on the custom object Ticket__c
that assigns each new Ticket's owner using round-robin logic across active support agents
for its Category__c.
Supporting Objects
This design needs two small helper custom objects to make the round-robin state durable
across transactions:
Support_Agent__c — User__c (lookup to User), Category__c, Active__c (Checkbox)
Round_Robin_Tracker__c — Category__c, Last_Index__c (Number) — stores the last-assigned agent index per category
Best Practices
- Query the
Round_Robin_Tracker__c row with FOR UPDATE so two
simultaneous Ticket inserts can't both land on the same agent.
upsert the tracker so a category with no prior tracker still gets one created.
Objects / Fields
Ticket__c — Category__c, OwnerId
Approach
- 1Build a Map> of active agent User Ids per category, ordered consistently (e.g. ORDER BY Name).
- 2Query existing Round_Robin_Tracker__c rows FOR UPDATE, keyed by Category__c.
- 3For each new Ticket, compute nextIndex = Math.mod(lastIndex + 1, agents.size()); assign OwnerId = agents[nextIndex]; update lastIndex for that category.
- 4upsert the tracker records once after the loop.
Hard
TriggersRollup
61. Roll Up Average Feedback Rating Onto the Product (Bulk-Safe)
Problem #384 · Salesforce Apex Coding Challenge
Problem Statement
Write an after insert trigger on the custom object Feedback__c
that calculates the average rating and updates a custom field Average_Rating__c
on the related custom object Product__c, handling bulk (200+ record) inserts
efficiently without hitting governor limits.
Best Practices
- Use an aggregate
AVG(...) query grouped by Product__c — this
computes every Product's average in one SOQL call regardless of batch size.
- Dedup target Products with a
Set<Id> so each Product is updated exactly
once even if dozens of Feedback records in the batch point to the same Product.
Objects / Fields
Feedback__c — Product__c (lookup), Rating__c (Number)
Product__c.Average_Rating__c (custom Number)
Approach
- 1Collect Product Ids from the new Feedback records into a Set.
- 2Run a single aggregate query averaging Rating__c for those Products, grouped by Product__c.
- 3Build one Product__c update per Id in the set, using totals.get(id).setScale(2) — default to 0 if there is no entry.
- 4Test with a mix of Products and multiple Feedback records each, to prove the aggregate approach scales without extra queries.
Medium
Triggers
62. Prevent Account Owner Change When Pending Approval Exists
Problem #544 · Salesforce Apex Coding Challenge
Problem Statement
Write a trigger PreventOwnerChangeDuringApproval on Account
that fires before update.
If an Account's OwnerId is being changed while that Account has a
pending approval request (a ProcessInstance with
Status = 'Pending'), block the change with addError().
Requirements
- Only check Accounts whose
OwnerId is actually changing.
- Use a single bulk SOQL query against
ProcessInstance, outside any loop.
- Bulk-safe: handle multiple Account updates in one transaction.
Approach
- 1Compare
acc.OwnerId against Trigger.oldMap.get(acc.Id).OwnerId to detect an owner change. - 2Query
ProcessInstance with WHERE TargetObjectId IN :accountIds AND Status = 'Pending' — one bulk call, not per record. - 3Collect matching Account Ids into a Set, then loop
Trigger.new once more to call addError().
Medium
Triggers
63. Synchronize Opportunity Type With Account Customer Segment
Problem #547 · Salesforce Apex Coding Challenge
Problem Statement
Write a trigger SyncOpportunityTypeWithSegment on Opportunity
that fires on before insert and before update.
Automatically set the Opportunity's Type based on the parent Account's
Customer_Segment__c:
Enterprise → New Customer
Mid-Market → Existing Customer - Upgrade
Small Business → Existing Customer - Replacement
Requirements
- Use a single bulk SOQL query against Account, outside any loop.
- Leave
Type untouched if the segment doesn't match a known mapping.
Approach
- 1A
Map<Id, Account> built directly from a SOQL query gives you Id-keyed lookups for free. - 2A
Map<String, String> literal is a clean way to express the segment → Type mapping. - 3Skip setting
Type entirely when the segment isn't in the map, so an unrelated value is never overwritten with something wrong.
Hard
Triggers
64. Update Account Health Status From Related Cases
Problem #550 · Salesforce Apex Coding Challenge
Problem Statement
Write a trigger UpdateAccountHealthFromCases on Case that
keeps each related Account's Health_Status__c in sync with its open,
high-priority Case load.
Whenever an Account has 3 or more open (IsClosed = false)
Cases with Priority = 'High', set Health_Status__c to
'Critical'; otherwise set it to 'Healthy'.
Requirements
- Fire on after insert, after update, after delete, and after undelete —
any Case change can shift the count.
- Recompute using a single aggregate SOQL query (
GROUP BY AccountId),
not a loop.
- Bulk-safe: recompute every affected Account in one query and one DML update.
Approach
- 1
Trigger.isDelete means the records live in Trigger.old, not Trigger.new. - 2An aggregate query like
SELECT AccountId, COUNT(Id) cnt FROM Case WHERE ... GROUP BY AccountId gives you a count per Account in one call. - 3Accounts with zero matching Cases won't appear in the aggregate result at all — default their count to 0 rather than skipping them.
Medium
Triggers
65. Copy Opportunity Primary Contact Phone to Custom Field
Problem #551 · Salesforce Apex Coding Challenge
Problem Statement
Write a trigger SyncPrimaryContactPhone on Opportunity that
fires on before insert and before update.
Whenever the Opportunity's Primary_Contact__c lookup is set or changed,
copy that Contact's Phone into Primary_Contact_Phone__c. If
Primary_Contact__c is cleared, clear the phone field too.
Requirements
- Use a single bulk SOQL query against Contact, outside any loop.
- On update, only re-query Contacts whose
Primary_Contact__c actually changed.
Approach
- 1On insert there's no
Trigger.oldMap — guard the "did it change" check with Trigger.isInsert || first so it short-circuits before touching oldMap. - 2A
Map<Id, Contact> built directly from the SOQL query gives you a Phone lookup by Contact Id.
Medium
Triggers
66. Automatically Flag Accounts With Expiring Contracts
Problem #552 · Salesforce Apex Coding Challenge
Problem Statement
Write a trigger FlagAccountsWithExpiringContracts on Contract
that fires on after insert and after update.
Set the parent Account's Contract_Expiring__c checkbox to true
when any of its Activated Contracts has an EndDate within the
next 30 days (inclusive); otherwise set it to false.
Requirements
- Use a single bulk SOQL query against Contract, outside any loop.
- Only consider Contracts with
Status = 'Activated'.
A Note on Time-Based Logic
Because "within 30 days" changes as time passes with no record edit at all, a trigger
alone can only re-evaluate an Account when one of its Contracts is actually written to.
A production implementation would pair this trigger with a nightly Schedulable/Batch job
to catch a Contract simply aging into the 30-day window.
Approach
- 1
Date.today().addDays(30) gives you the cutoff date. - 2Filter with
EndDate >= TODAY AND EndDate <= :cutoff so past-dated Contracts don't count. - 3Every affected Account needs an explicit true/false — don't skip Accounts with zero expiring Contracts, they need to be reset to false too.
Hard
Triggers
67. Prevent Multiple Primary Contacts Per Account
Problem #553 · Salesforce Apex Coding Challenge
Problem Statement
Write a trigger PreventMultiplePrimaryContacts on Contact
that fires on before insert and before update.
An Account may have at most one Contact with Is_Primary_Contact__c = true.
Block, with addError(), any Contact save that would give an Account a second
primary Contact.
Requirements
- Use a single bulk SOQL query against Contact to find each Account's existing
primary Contact, outside any loop.
- Bulk-safe: two new Contacts both claiming primary for the same Account in
the same transaction must not both be allowed through.
- Updating the Account's existing primary Contact (keeping it primary) must not
conflict with itself.
Approach
- 1On insert,
con.Id is null — comparing it to an existing primary's Id correctly identifies "some other, already-saved record already holds this". - 2Track Account Ids "claimed" earlier in the same loop with a
Set<Id> so two new Contacts in one insert batch can't both slip through.
Medium
Triggers
68. Scenario: Prevent Duplicate Support Cases from the Same Contact Within One Hour
Problem #439 · Salesforce Apex Coding Challenge
Business Scenario
Frustrated customers sometimes submit the same issue multiple times in quick succession
through different channels, creating duplicate Cases. Block a new Case from being created
if the same Contact already has a Case created within the last 60 minutes.
Requirements
- Collect all Contact Ids from the inserted Cases first, then run
one aggregate query — never query per record.
- Count each Contact's recent Cases created within the last 60 minutes, grouped
per Contact.
- Block (via
addError) any new Case whose Contact already has a recent Case.
Best Practices
- Bulkify by collecting all Contact Ids into a
Set<Id> first, then
running a single GROUP BY aggregate query — this scales to a bulk API insert of
hundreds of Cases in one query.
addError() on the new record is the standard way to block a save from a
before insert trigger.
Approach
- 1Collect ContactIds into a Set in one pass over Trigger.new, skipping any null ContactId.
- 2DateTime.now().addMinutes(-60) gives the cutoff for "within the last hour".
- 3A single aggregate query, grouped by ContactId, can count every Contact's recent Cases at once — no need to query per Contact.
- 4c.addError('...') on the new Case blocks the insert with a message the user sees.
Medium
Triggers
69. Scenario: Track the Highest-Value Open Opportunity Per Account in Real Time
Problem #440 · Salesforce Apex Coding Challenge
Business Scenario
Sales leadership wants each Account's custom field
Top_Open_Opportunity_Amount__c to always reflect the largest still-open
Opportunity on that Account, updated the moment any Opportunity changes — insert, update,
delete, or undelete.
Requirements
- Handle all four trigger contexts: insert, update, delete, undelete.
- Recompute each affected Account's largest still-open Opportunity amount with a
single aggregate query covering the whole batch — never one query per Account.
- An Account with no open Opportunities left should reset to
0, not stay stale.
Best Practices
- This must be an after trigger since it re-queries the Opportunity
table including the just-changed records.
Trigger.old is used for the delete context (there is no
Trigger.new on delete); every other context uses Trigger.new.
- Recompute from scratch via
MAX() rather than comparing only the changed
record's Amount — that's the only way a delete of the current top Opportunity
correctly falls back to the next-highest one.
Approach
- 1Trigger.isDelete tells you to read from Trigger.old instead of Trigger.new — there is no Trigger.new on a delete.
- 2A single aggregate query, filtered to open Opportunities and grouped per Account, can find every affected Account's new top amount at once.
- 3Default to 0 for any Account not present in the aggregate result — that means it has no open Opportunities left.
- 4This must be an after trigger, not before — it needs to see the fully-committed state of the just-changed Opportunity to query against it.
Medium
Triggers
70. Scenario: Notify the Account Owner When a Case Reaches Critical Priority
Problem #443 · Salesforce Apex Coding Challenge
Business Scenario
When a Case's Priority becomes "Critical" — either set that way
on creation, or changed to it later — create a follow-up Task on the related
Account so the account team notices immediately.
Requirements
- Fires on insert (created already Critical) and on update (changed to Critical).
- Must not re-fire on every subsequent edit of an already-Critical Case —
only the transition into Critical creates a new Task.
- Skip Cases with no related Account.
Best Practices
- Comparing against
Trigger.oldMap is how you detect a genuine
transition into a value, rather than firing on every save where the value happens
to already be Critical.
- Collect every Task into one list and insert once, outside the loop.
Approach
- 1Trigger.isInsert is true for newly-created Cases that are already Critical.
- 2On update, Trigger.oldMap.get(c.Id).Priority lets you check the PRIOR value — only a change away from Critical-already-set should count as a new transition.
- 3Skip AccountId == null Cases — there's no Account to attach the Task to.
- 4Collect all Tasks into one List and call insert once outside the loop.
Hard
Triggers
71. Scenario: Flag Potential Duplicate Opportunities on the Same Account and Close Month
Problem #447 · Salesforce Apex Coding Challenge
Business Scenario
Reps occasionally create a second Opportunity for the same deal instead of updating the
existing one. Flag a new Opportunity as a potential duplicate when the same Account already
has another open Opportunity closing in the same month and year.
Requirements
- Collect Account Ids from the new Opportunities first, then run a single aggregate query
grouped by Account, close month, and close year.
- Set
Potential_Duplicate__c = true on any new Opportunity whose
Account+month+year combination already has an existing open Opportunity.
- Skip Opportunities missing an Account or a Close Date.
Best Practices
CALENDAR_MONTH()/CALENDAR_YEAR() in a
GROUP BY aggregate is the standard way to bucket existing records by month
without pulling every record into Apex to compare dates manually.
- Building a composite string key (Account + year + month) lets you do one bulk lookup
instead of a nested loop comparing every new Opportunity against every existing one.
Approach
- 1CALENDAR_MONTH(CloseDate) and CALENDAR_YEAR(CloseDate) can both appear in the same GROUP BY alongside AccountId.
- 2Build a String key like accountId + '-' + year + '-' + month for both the aggregate results and each new Opportunity, so you can look up matches with a single Map.
- 3o.CloseDate.year() and o.CloseDate.month() give you the components to build the same key for each new record.
Medium
Triggers
72. Create a Case When a High-Value Opportunity Is Lost
Problem #472 · Salesforce Apex Coding Challenge
Business Scenario
Sales leadership wants visibility into every high-value deal that falls through. When
an Opportunity with Amount >= 100,000 transitions to Closed
Lost, automatically open an internal Case for the team to review
what happened.
Requirements
- After update trigger on
Opportunity.
- Only fire on a genuine transition into Closed Lost (not already Closed Lost), and
only when Amount meets the high-value threshold.
- The Case should reference the Opportunity's Account and summarize the lost deal in
its Description.
Best Practices
- Named constant for the high-value threshold, so the bar for "review-worthy" deals is
a one-line change.
- Collect all qualifying Cases into a list first and insert once — bulk-safe even if
many high-value deals are lost in the same batch update.
- Wrap the insert in
try/catch(DmlException).
Approach
- 1Combine two Booleans — justLost and isHighValue — then only create the Case when both are true.
- 2AccountId = newOpp.AccountId links the Case straight to the Account for easy triage.
- 3Guard Amount against null before comparing it to the threshold.
Medium
Triggers
73. Prevent Duplicate Product Entries in an Opportunity
Problem #473 · Salesforce Apex Coding Challenge
Business Scenario
Sales operations wants to stop reps from accidentally adding the same Product twice to
an Opportunity's line items, which throws off quantity and revenue totals.
Requirements
- Before insert trigger on
OpportunityLineItem.
- Block a new line item whose
Product2Id already exists on the same
Opportunity, using addError().
- Also catch duplicates within the same insert batch — two new line items for
the same Opportunity and Product added in one call must not both slip through.
Best Practices
- Query existing line items for all affected Opportunities in a single SOQL call, then
check every new line item against the results in memory — no query per record.
- A second in-memory Map tracks products already seen earlier in the same Trigger.new
batch, since a same-transaction duplicate would not appear in the existing-records
query.
Approach
- 1PricebookEntry.Product2Id (or the OpportunityLineItem.Product2Id shortcut field) identifies the product on a line item.
- 2You need two duplicate checks: against already-saved records (from the query) and against other new records in the same Trigger.new list.
- 3A Map> keyed by OpportunityId lets you check "is this product already used on this specific Opportunity" in O(1).
Medium
Triggers
74. Update Contact Loyalty Status Based on Total Purchases
Problem #474 · Salesforce Apex Coding Challenge
Business Scenario
Customer Success wants a Contact's loyalty tier kept in sync with their total Closed
Won purchase history. Opportunities carry a custom lookup Primary_Contact__c
to Contact, and Contact has a custom picklist
Loyalty_Status__c (Standard/Silver/Gold/Platinum).
Requirements
- After update trigger on
Opportunity, firing when an
Opportunity transitions into Closed Won.
- Recompute the Contact's total Closed Won Amount (not just this one deal)
and set the tier: Platinum ≥ 100,000, Gold ≥ 50,000, Silver ≥ 10,000, else Standard.
- Only update Contacts whose tier actually changes.
Best Practices
- Use an aggregate
SUM(Amount) ... GROUP BY Primary_Contact__c query to
get each Contact's true lifetime total in one query, rather than summing in Apex from a
detail query.
- Compare the computed tier against the Contact's current
Loyalty_Status__c before adding it to the update list — skip Contacts whose
tier is unchanged to avoid a redundant DML write.
- Wrap the update in
try/catch(DmlException).
Approach
- 1GROUP BY Primary_Contact__c with SUM(Amount) gives each Contact's true lifetime Closed Won total in one query.
- 2Use nested ternaries (or if/else if) ordered from the highest tier down to compute the new status string.
- 3Compare newStatus against the Contact's current Loyalty_Status__c and only add to the update list when they differ.
Medium
Triggers
75. Create Renewal Opportunities Automatically
Problem #475 · Salesforce Apex Coding Challenge
Business Scenario
Renewals should never fall through the cracks. When a renewable deal (custom checkbox
Is_Renewable__c on Opportunity) closes as Closed Won, a follow-up
renewal Opportunity should be created automatically one contract term (12 months) out, so
the account team has it on their pipeline well in advance.
Requirements
- After update trigger on
Opportunity.
- Only act on a transition into Closed Won where
Is_Renewable__c is
true.
- The new Opportunity copies the Account and Amount, starts in
Prospecting, has a CloseDate 12 months after the original, and tracks its
origin via a custom lookup Renewed_From__c.
Best Practices
- Collect all renewal Opportunities to insert into a list first, then insert once —
bulk-safe even if many renewable deals close in the same transaction.
- Named constant for the renewal term length keeps the "how far out" business rule
easy to tune.
- Wrap the insert in
try/catch(DmlException).
Approach
- 1newOpp.CloseDate.addMonths(RENEWAL_TERM_MONTHS) computes the renewal Opportunity's CloseDate.
- 2Set Renewed_From__c = newOpp.Id so the new Opportunity traces back to the deal that spawned it.
- 3Only build a renewal when both the stage transition AND Is_Renewable__c == true are satisfied.
Hard
Triggers
76. Prevent Changing Account Owner for VIP Customers
Problem #476 · Salesforce Apex Coding Challenge
Business Scenario
Executive sponsors insist that VIP customer Accounts (custom checkbox
VIP_Customer__c) can only have their owner reassigned by specially authorized
users, to prevent accidental or unauthorized handoffs of the company's most important
relationships.
Requirements
- Before update trigger on
Account.
- Detect when
OwnerId is actually changing on a VIP Account (compare
against Trigger.oldMap).
- Block the change with
addError() unless the running user holds a custom
permission Reassign_VIP_Accounts.
Best Practices
FeatureManagement.checkPermission() checks a Custom Permission on the
running user — a declarative, admin-configurable way to gate this override without
hardcoding specific User Ids or Profiles in Apex.
- Use
Trigger.oldMap (not a loop-built Map) since before
update guarantees it is already populated and index-aligned to
Trigger.new by Id.
- Bulk-safe: the permission is checked once, not per record, and every qualifying
record in the batch gets its own
addError().
Approach
- 1Trigger.oldMap.get(newAcc.Id) gives you the pre-update version of the same record to compare OwnerId against.
- 2FeatureManagement.checkPermission('Reassign_VIP_Accounts') returns a Boolean for the running user's Custom Permission — call it once outside the loop.
- 3Only records where both VIP_Customer__c is true and OwnerId changed should ever be blocked.
Hard
Triggers
77. Update Parent Account Risk Level Based on Open Cases
Problem #477 · Salesforce Apex Coding Challenge
Business Scenario
Support leadership wants an Account's Risk_Level__c (custom picklist:
Low/Medium/High) to reflect how many open Cases it currently has,
recalculated whenever Cases are inserted, updated, deleted, or undeleted.
Requirements
- Trigger on
Case for after insert, after update, after delete,
after undelete.
- Risk is High with 5+ open Cases, Medium with 2-4, Low otherwise.
- Recompute using the true current open-Case count from the database (not just the
records in the trigger batch), since Cases unrelated to this transaction also affect the
count.
- Only update Accounts whose computed risk level actually changes.
Best Practices
- Use
Trigger.isDelete to pick Trigger.old vs
Trigger.new as the source of affected Cases, since Trigger.new
is not available in a delete context.
- A single aggregate query grouped by Account gets the authoritative open-Case count
per Account, rather than trying to infer it from just the records in the trigger batch.
- Compare the newly computed risk level against the Account's current value before
adding it to the update list, avoiding a no-op DML write.
Approach
- 1Trigger.new does not exist in an after delete context — use Trigger.isDelete to choose Trigger.old instead.
- 2COUNT(Id) ... WHERE IsClosed = false GROUP BY AccountId gives the true current open-Case count regardless of what triggered this run.
- 3Order your risk-level ternary from High down to Low so the highest-matching tier wins.
Hard
Triggers
78. Prevent Opportunity Closure If Mandatory Documents Are Missing
Problem #478 · Salesforce Apex Coding Challenge
Business Scenario
Deal desk requires at least one supporting file (signed contract, approval memo, etc.)
to be attached to an Opportunity before it can be closed, whether won or lost, to maintain
an audit trail.
Requirements
- Before update trigger on
Opportunity.
- Detect a transition into
Closed Won or Closed Lost from any
non-closed stage.
- Block the transition with
addError() if the Opportunity has zero
attached Files (query ContentDocumentLink).
Best Practices
ContentDocumentLink.LinkedEntityId is how Salesforce Files relate to any
parent record — an aggregate COUNT(Id) ... GROUP BY LinkedEntityId query
gets attachment counts for every closing Opportunity in one call.
- A
Set<String> of closing stage names avoids duplicating the
"Closed Won or Closed Lost" check as two separate conditions everywhere it's needed.
- Only Opportunities genuinely transitioning into a closed stage are checked — editing
an already-closed Opportunity for an unrelated field should not re-trigger this
validation.
Approach
- 1CLOSING_STAGES.contains(newOpp.StageName) && !CLOSING_STAGES.contains(oldOpp.StageName) detects entering a closed stage from an open one.
- 2ContentDocumentLink.LinkedEntityId holds the Id of whatever record a File is attached to — filter WHERE LinkedEntityId IN :opportunityIdsClosing.
- 3Treat an Opportunity missing entirely from the aggregate result as having 0 documents.
Hard
Triggers
79. Synchronize Custom Object Records Automatically
Problem #479 · Salesforce Apex Coding Challenge
Business Scenario
Every Product2 can have one or more related Warranty__c
records (custom object: Product__c lookup to Product2,
Status__c picklist Active/Discontinued). When a Product is deactivated or
reactivated (IsActive changes), every one of its Warranty records must be
synchronized to match — discontinued products should never keep an "Active" warranty.
Requirements
- After update trigger on
Product2.
- Only act when
IsActive actually changed on the Product.
- Update every related
Warranty__c's Status__c to match the
Product's new active state, skipping Warranties that already match.
Best Practices
- Detect the real field-level change (
IsActive differs from
Trigger.oldMap) before doing any query — Products saved with unrelated
field edits should not trigger a Warranty sync at all.
- One SOQL query pulls every affected Warranty for every changed Product, and one bulk
DML call updates them — no per-Product query or update.
- Skip Warranties whose
Status__c already matches the expected value to
avoid a no-op write.
Approach
- 1Compare newProd.IsActive != oldProd.IsActive to detect a genuine change, not just any update.
- 2Map productsById built from Trigger.new gives O(1) lookup of the parent Product2 for each Warranty.
- 3parentProduct.IsActive ? 'Active' : 'Discontinued' computes the expected Warranty status directly from the Product's new state.
About ApexArena
ApexArena is a free, browser-based Salesforce Apex coding practice platform covering every major topic tested on the Salesforce Platform Developer I (PD1) and Platform Developer II (PD2) certification exams. All problems run directly in your browser with instant pass/fail feedback, best-practice linting (SOQL in loops, DML in loops, empty catch blocks), and governor limit monitoring — no Salesforce Developer Edition org required.
Related tutorials:
Apex Triggers ·
SOQL ·
Batch Apex ·
Interview Q&A ·
Governor Limits