Apex Trigger

Medium & Hard Apex Trigger Practice Problems

61 free Salesforce Apex challenges · Full problem statements, best-practice notes, and step-by-step hints · Solve live on ApexArena

This guide walks through 61 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.

On this page
  1. 1. Contact Count Rollup Trigger
  2. 2. Recursive Trigger Guard
  3. 3. Contact Email Deduplication
  4. 4. Opportunity Won Revenue Rollup
  5. 5. Prevent Duplicate Contacts by Email
  6. 6. Auto-Close Stale Opportunities
  7. 7. Case Count Rollup on Account
  8. 8. Dynamic Required Field Enforcement
  9. 9. Sync Account Phone to Child Contacts
  10. 10. Stage Change Close Date Setter
  11. 11. Account Deletion Blocker
  12. 12. Opportunity Undelete Restorer
  13. 13. Deactivate Account Validation Trigger
  14. 14. Employee Termination Validation Trigger
  15. 15. Cross-Object IsActive Checkbox Sync
  16. 16. Country-wise Account Count Tracker
  17. 17. Total Contact Count Rollup on Account
  18. 18. Sync Email Field Across Sibling Opportunities
  19. 19. Auto-Reassign Accounts & Opportunities on User Deactivation
  20. 20. Trigger: Set Closed-Won Date on Opportunity Stage Change
  21. 21. Trigger: Rollup Contact Count onto Account
  22. 22. Trigger: Auto-Create Follow-Up Task for High Priority Cases
  23. 23. Trigger: Auto-Calculate Lead Score
  24. 24. Trigger: Block Opportunity Stage Regression
  25. 25. Trigger: Prevent Duplicate Contact Email
  26. 26. Trigger + Handler: Update Campaign Stats on Closed Won
  27. 27. Trigger + Handler: Escalate Case Priority on SLA Breach
  28. 28. Trigger + Handler: Auto-Assign Case Owner by Support Region
  29. 29. Trigger + Handler: Block Account Deletion When Open Opportunities Exist
  30. 30. Trigger + Handler: Rollup Closed-Won Revenue onto Account
  31. 31. Apex Trigger: Auto-Set Opportunity Probability on Stage Change
  32. 32. Trigger + Handler: Prevent Duplicate Lead Email on Insert and Update
  33. 33. Apex Trigger: Auto-Populate Contact Description from Account on Insert
  34. 34. Trigger + Handler: Auto-Format Contact Phone on Insert/Update
  35. 35. Trigger + Handler: Block Opportunity Close When Amount Is Null
  36. 36. Trigger + Handler: Rollup Open Opportunity Count onto Account
  37. 37. Trigger + Handler: Block Contact Deletion When Linked to Closed Won Opportunity
  38. 38. Trigger: Maintain Contact Count Rollup on Account
  39. 39. Trigger: Enforce Unique Email Across All Leads
  40. 40. Trigger: Cascade Account Billing Address to Related Contacts
  41. 41. Trigger: Rollup Sum from Child Custom Object to Parent
  42. 42. Trigger: Flag Out_of_Zip__c When Contact Postal Code Differs
  43. 43. Trigger: Auto-Create Parent Account for Orphaned Contacts
  44. 44. Trigger: Flag needintel__c When 70%+ Contacts Are Inactive
  45. 45. Trigger: Update Contact Profile URL When Account Website Changes
  46. 46. Trigger: Flag is_gold__c on Accounts with Opportunities Over $20,000
  47. 47. Copy Account Billing Address to New Contact
  48. 48. Auto-Create Follow-Up Task for High-Priority Cases
  49. 49. Cascade Account Status Change to Related Contacts
  50. 50. Block Contact Deletion When Linked to an Open Opportunity
  51. 51. Update Last Activity Date on Account/Contact From Task
  52. 52. Block Project Completion While Child Tasks Are Open
  53. 53. Validate Timesheet Weekly Hours Do Not Exceed 40
  54. 54. Block Vendor Deletion While Active Purchase Orders Exist
  55. 55. Roll Up Open Opportunity Amount Onto the Account
  56. 56. Auto-Escalate Cases Older Than 5 Days
  57. 57. Roll Up Expense Amount to Parent Project (Full DML Coverage)
  58. 58. Decrement Product Stock When an Order Item Is Created
  59. 59. Email the Owner When a Contract Expires
  60. 60. Round-Robin Assign New Tickets to Support Agents
  61. 61. Roll Up Average Feedback Rating Onto the Product (Bulk-Safe)
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

  • AccountIsActive__c (Checkbox custom field)
  • OpportunityAccountId, StageName
Approach
  • 1In the handler, build a Set of Account Ids being deactivated (IsActive__c changed from true → false).
  • 2Query Opportunities: WHERE AccountId IN :deactivatingIds AND StageName NOT IN ('Closed Won', '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__cStatus__c (picklist: Active, On Leave, Terminated)
  • Employee_Review__cEmployee__c (Lookup to Employee__c)
Approach
  • 1Collect Ids where Status__c is changing to "Terminated".
  • 2Query: SELECT Employee__c, COUNT(Id) cnt FROM Employee_Review__c WHERE Employee__c IN :ids GROUP BY Employee__c.
  • 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: SELECT Id FROM Contact WHERE AccountId IN :ids AND IsActive__c = true.
  • 3One query for Users: SELECT Id FROM User WHERE Id IN :ownerIds AND IsActive__c = true.
  • 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: SELECT BillingCountry, COUNT(Id) cnt FROM Account WHERE BillingCountry IN :countries GROUP BY BillingCountry.
  • 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: SELECT AccountId, COUNT(Id) cnt FROM Contact WHERE AccountId IN :ids GROUP BY AccountId.
  • 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 all sibling Opps: SELECT Id, AccountId, Contact_Email__c FROM Opportunity WHERE AccountId IN :changedAccountIds AND Id NOT IN :triggerIds.
  • 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 where OwnerId IN :deactivatedIds.
  • 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: SELECT AccountId, COUNT(Id) cnt FROM Contact WHERE AccountId IN :ids GROUP BY AccountId
  • 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.
  • Use AggregateResult to read the count: (Integer) ar.get('cnt').
  • 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.
  • 2AggregateResult query: SELECT AccountId, COUNT(Id) cnt FROM Contact WHERE AccountId IN :accountIds GROUP BY AccountId.
  • 3Cast the count: Integer cnt = (Integer) ar.get('cnt'); then set acc.Contact_Count__c = cnt.
  • 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:

FieldConditionPoints
LeadSourceWeb+10
LeadSourcePhone Inquiry+8
LeadSourceOther+5
TitleContains 'VP' or 'Director'+15
TitleContains 'Manager'+8
CompanyNot blank+5
EmailNot 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

  1. Collect non-blank emails from Trigger.new.
  2. Run one SOQL: existing contacts with matching email, excluding records being updated (Id NOT IN :Trigger.newMap.keySet()).
  3. Build a Set<String> of existing emails (lowercased).
  4. 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.
  • 2SOQL: SELECT Email FROM Contact WHERE Email IN :emailsToCheck AND Id NOT IN :newIds
  • 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

  1. Find opps where StageName changed to 'Closed Won' and CampaignId != null.
  2. Collect unique CampaignIds.
  3. Query campaigns once: SELECT Id, Won_Opportunities__c FROM Campaign WHERE Id IN :campaignIds.
  4. 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'
  • 2SOQL: [SELECT Id, Won_Opportunities__c FROM Campaign WHERE Id IN :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

  1. Collect unique Region__c values from Trigger.new.
  2. Query Group (queues) where Type = 'Queue' and Name IN :regions.
  3. Build a Map<String, Id> of region name → queue Id.
  4. 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: [SELECT Id, Name FROM Group WHERE Type = 'Queue' AND Name IN :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

  1. Collect all Account Ids from Trigger.old.
  2. Query open Opportunities: WHERE AccountId IN :accountIds AND IsClosed = false.
  3. Build a Map<Id, List<Opportunity>> grouped by AccountId.
  4. 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: [SELECT Id, AccountId FROM Opportunity WHERE AccountId IN :accountIds AND IsClosed = false]
  • 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

  1. Trigger collects affected Account Ids (from Trigger.new AND Trigger.old for updates, Trigger.old for deletes).
  2. Handler uses one AggregateResult SOQL with SUM(Amount) GROUP BY AccountId.
  3. 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.
  • 2Aggregate: [SELECT AccountId, SUM(Amount) total FROM Opportunity WHERE AccountId IN :ids AND IsClosed = true AND IsWon = true GROUP BY AccountId]
  • 3Read aggregate value: (Decimal) ar.get('total')
  • 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

StageProbability
Prospecting10
Qualification20
Needs Analysis30
Value Proposition40
Id. Decision Makers50
Perception Analysis60
Proposal/Price Quote70
Negotiation/Review80
Closed Won100
Closed Lost0

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.

Halfway there — solve them live

Every problem above runs in a real in-browser Apex editor with instant pass/fail feedback and best-practice linting. No Salesforce org needed.

Create Free Account →
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

  1. Skip blank emails and unchanged emails on update.
  2. Query existing unconverted Leads with matching emails, excluding the records being updated (Id NOT IN :currentIds).
  3. 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: Map accountMap = new Map([SELECT Id, Description FROM Account WHERE Id IN :accountIds]);
  • 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 eventif (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.
  • 2Aggregate query: SELECT AccountId, COUNT(Id) cnt FROM Contact WHERE AccountId IN :accountIds GROUP BY AccountId.
  • 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: SELECT Id, Email FROM Lead WHERE Email IN :emailSet.
  • 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.
  • 2Aggregate: SELECT CustomObjectA__c, SUM(Value__c) totalValue FROM CustomObjectB__c WHERE CustomObjectA__c IN :parentIds GROUP BY CustomObjectA__c.
  • 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: SELECT AccountId, MailingPostalCode, Account.BillingPostalCode FROM Contact WHERE AccountId IN :changedIds.
  • 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.
  • Calculate: (deadCount * 100 / totalCount) >= 70.
  • 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
  • 1Total query: SELECT AccountId, COUNT(Id) cnt FROM Contact WHERE AccountId IN :accIds GROUP BY AccountId.
  • 2Dead query: same as total but add AND Dead__c = true to the WHERE clause.
  • 3Percentage check: if (total > 0 && (dead * 100 / total) >= 70) — integer division.
  • 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 SmithProfile__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
  • 1Check: acc.Website != Trigger.oldMap.get(acc.Id).Website && acc.Website != null.
  • 2The SOQL already retrieves Account.Website — use con.Account.Website in the loop.
  • 3Build the URL: con.Account.Website + '/' + con.FirstName.substring(0,1) + con.LastName.
  • 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: SELECT AccountId FROM Opportunity WHERE AccountId IN :accountIds AND Amount > 20000.
  • 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

  • ContactAccountId, MailingStreet/City/State/PostalCode/Country
  • AccountBillingStreet/City/State/PostalCode/Country
Approach
  • 1Only collect AccountIds for Contacts where MailingStreet AND MailingCity are both null.
  • 2Query the Accounts in one call: SELECT BillingStreet, BillingCity, ... FROM Account WHERE Id IN :accountIds.
  • 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

  • CasePriority, OwnerId, CaseNumber
  • TaskWhatId, 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

  • AccountAccount_Status__c (custom Text/Picklist)
  • ContactAccountId, 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 [SELECT Id, AccountId FROM Contact WHERE AccountId IN :accountIds], 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 [SELECT ContactId FROM OpportunityContactRole WHERE ContactId IN :contactIds AND Opportunity.StageName != 'Closed Lost'].
  • 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

  • TaskWhatId, 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__cStatus__c
  • Task__cProject__c (lookup), Status__c
Approach
  • 1Collect a Set of Projects where Status__c is becoming Completed (new == Completed, old != Completed).
  • 2Query [SELECT Project__c FROM Task__c WHERE Project__c IN :becomingComplete AND Status__c = '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__cEmployee__c (lookup), Week_Start_Date__c (Date), Hours__c (Number)
Approach
  • 1Query [SELECT Employee__c empId, Week_Start_Date__c weekStart, SUM(Hours__c) totalHours FROM Timesheet__c WHERE Employee__c IN :employeeIds GROUP BY Employee__c, Week_Start_Date__c] once.
  • 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 [SELECT Vendor__c FROM Purchase_Order__c WHERE Vendor__c IN :vendorIds AND Status__c = 'Active'].
  • 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

  • OpportunityAccountId, 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.
  • 3Query [SELECT AccountId accId, SUM(Amount) total FROM Opportunity WHERE AccountId IN :accountIds AND IsClosed = false GROUP 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

  • CaseStatus, 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__cProject__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.
  • 3Query [SELECT Project__c projId, SUM(Amount__c) total FROM Expense__c WHERE Project__c IN :projectIds GROUP 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__cProduct__c (lookup), Quantity__c (Number)
  • Product__c.Quantity_In_Stock__c (Number)
Approach
  • 1Collect Product Ids from the Order Items into a Set.
  • 2Query [SELECT Id, Quantity_In_Stock__c FROM Product__c WHERE Id IN :productIds FOR UPDATE] 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__cContract_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__cUser__c (lookup to User), Category__c, Active__c (Checkbox)
  • Round_Robin_Tracker__cCategory__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__cCategory__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__cProduct__c (lookup), Rating__c (Number)
  • Product__c.Average_Rating__c (custom Number)
Approach
  • 1Collect Product Ids from the new Feedback records into a Set.
  • 2Query [SELECT Product__c prodId, AVG(Rating__c) avgRating FROM Feedback__c WHERE Product__c IN :productIds GROUP 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.

Practice All 61 Problems Free

Write and run real Apex code right in your browser — instant pass/fail feedback, best-practice linting, and governor limit monitoring. No Salesforce org needed.

Create Free Account → Explore All Problems
More Practice Topics
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