Explore Recursion With Scenario-1
Explore Recursion With Scenario-1
1. Recursive Trigger
A recursive trigger in Salesforce is when a trigger accidentally calls itself, causing a loop of repeated
executions.
16 16
Total stack depth for any Apex invocation that recursively fires
(Synchronous (Asynchronous
triggers due to insert, update, or delete statements
Apex) Apex)
When recursive Apex code runs (Apex code that calls itself or loops), it can behave in two different
If your recursive Apex code doesn't cause any triggers to fire (like insert, update, or delete
operations), it runs within a single process or "stack." This is more efficient because everything
happens in one continuous flow.
If your Apex code does fire a trigger (for example, an insert or update operation causes a trigger
to run), that trigger starts a new process (a separate "invocation"). Each time the trigger fires, a new
instance of the Apex process is created. This is more resource-intensive because starting new
processes takes more time and uses more system resources.
1.1.c. Points to Remember:
• Recursive operations without triggers are lighter and can go deeper before hitting limits.
• Recursive operations with triggers are heavier and have stricter limits on how many times they
can loop (because each trigger creates a new process).
• That's why Salesforce puts tighter limits on recursion when triggers are involved—to prevent the
system from getting overloaded by too many separate processes running at once.
Here, I will implement below three scenarios using two methods Static Set for Record Ids
and Trigger Context Variables and analyze their performance and execution.
a. Scenario 1 :
Whenever the Account Phone field is updated, the new value must also be reflected in the
related Contact Phone field.
b. Scenario 2 :
When a Contact's Department field is ‘ Finance’, the priority of its related cases should be set
to High.
c. Scenario 3 :
Create a custom field titled "No. of High Priority Cases" on Account Object and update this
field to reflect the total number of high-priority cases when Case priority is set to ‘High’. Also, if the
status is ‘Escalated’, then that Case Number has to be updated in the Account Description Field.
The scenario described above creates recursion, as explained in the following image.
1.3.1. By Implementing those Scenario without any Recursion Prevention, the
following Error will be thrown.
1.3.2. Implementation With Recursion Prevention Method
Static Set For Ids
1.3.2.a. Scenario 1
//AccountTrigger
trigger AccountTrigger on Account (after update)
//CopyPhoneHandler
public class copyPhoneHandler
{
List<Account> triggerNewList;
List<Account> triggerOldList;
Map<Id,Account> triggerNewMap;
Map<Id,Account> triggerOldMap;
public copyPhoneHandler()
{
triggerNewList = (List<Account>)Trigger.New;
triggerOldList = (List<Account>)Trigger.Old;
triggerNewMap = (Map<Id,Account>)Trigger.NewMap;
triggerOldMap = (Map<Id,Account>)Trigger.OldMap;
}
public void doAction()
{
switch on Trigger.OperationType
{
When After_Update
{
for(Account accRecord:triggerNewList)
{
if(!recursiveIds.contains(accRecord.Id))
{
recursiveIds.add(accRecord.Id);
processIdSet.add(accRecord.Id);
}
}
if(!processIdSet.isEmpty())
{
updatePhoneToContact();
}
}
}
}
for(Contact conRecord:conList)
{
conRecord.Phone = triggerNewMap.get(conRecord.AccountId).Phone;
toUpdateContact.add(conRecord);
}
if(!toUpdateContact.isEmpty())
{
update toUpdateContact;
}
} }
1.3.2.b. Scenario 2
//ContactTrigger
trigger ContactTrigger on Contact (after update)
obj.doAction();
}
//ContactToCaseTriggerHandler
public class ContactToCaseTriggerHandler
{
List<Contact> triggerNewList;
List<Contact> triggerOldList;
Map<Id,Contact> triggerNewMap;
Map<Id,Contact> triggerOldMap;
public static Set<Id> recursiveIds = new Set<Id>();
public ContactToCaseTriggerHandler()
{
triggerNewList = (List<Contact>)Trigger.New;
triggerOldList = (List<Contact>)Trigger.Old;
triggerNewMap = (Map<Id,Contact>)Trigger.NewMap;
triggerOldMap = (Map<Id,Contact>)Trigger.OldMap;
}
public void doAction()
{
switch on Trigger.OperationType
{
When After_Update
{
for(Contact conRecord:triggerNewList)
{
if(!recursiveIds.contains(conRecord.Id))
{
recursiveIds.add(conRecord.Id);
}
}
updateCase();
}
}
}
public void updateCase()
{
List<Case> caseList = [Select Id,Priority,ContactId
From Case
Where ContactId In :triggerNewMap.keySet() And
Contact.Department = 'Finance' and Priority != 'High'
];
for(Case record:caseList)
{
record.Priority = 'High';
}
if(!caseList.isEmpty())
{
update caseList;
}
}
}
1.3.2.c. Scenario 3
//CaseTrigger
trigger CaseTrigger on Case (after update)
// CaseToAccountHandler
processList.add(record.AccountId);
recursiveIds.add(record.Id);
}
}
updateAccount();
}
}
}
//the below query is written to check whether description is added to Account or not
}
1.3.2.d. TEST CLASS
//TestClass
@isTest
public class RecursionTest
{
@testSetup
public static void loadData()
{
List<Account> accList = new List<Account>();
for(Integer i = 0;i<2;i++)
{
Account acc = new Account(Name = 'RecursionTest '+i);
accList.add(acc);
}
insert accList;
for(Integer i = 0;i<2;i++)
{
Case rec = new Case(Subject = 'RecursionTest Cont case '+i,
Origin = 'Phone',
ContactId = conList[0].Id);
caseList.add(rec);
}
insert caseList;
}
@isTest
public static void dotest()
{
List<Account> accList = [Select Id,Phone,Name
From Account
Where Name Like 'RecursionTest%'];
for(Account acc:accList)
{
acc.Phone = '2364758544';
}
}
}
1.3.2.e. Execution Log
'Finance,' Case records, update Account records with a phone number, and update Case records with the
status 'Escalated.'
However, due to recursion prevention, the *Update DML 2 Statement for the Case status is not
1.3.3.a. Scenario 1
//CopyPhoneHandler ( Part of Coding with Changes)
processIdSet.add(accRecord.Id);
}
}
if(!processIdSet.isEmpty())
updatePhoneToContact();
}
}
}
1.3.3.b. Scenario 2
//ContactToCaseTriggerHandler ( Part of Coding with Changes)
When After_Update
{
for(Contact conRecord:triggerNewList)
{
if(Trigger.isUpdate)
{
if(( triggerOldMap.get(conRecord.Id).Department !=
ConRecord.Department)
|| ( conRecord.Department == 'Finance' )
{
processIdSet.add(conRecord.Id);
}
}
}
if(!processIdSet.isEmpty())
updateCase();
}
----------
----------
------------
When After_Update
{
for(Case record:triggerNewList)
{
if( record.AccountId != Null)
{
if(Trigger.isUpdate)
{
//check high priority case is closed, custom field to be updated
if((( triggerOldMap.get(record.Id).Status != record.Status)
&&(record.Status == 'Closed' || record.Status == 'Escalated' )
&& ( record.Priority== 'High' ) )
||
((triggerOldMap.get(record.Id).Priority != record.Priority )
&& (( record.Priority== 'High') ||
(triggerOldMap.get(record.Id).Priority == 'High' ) )))
//check priority field is updated, either from high to other or other to
high
{
processIdSet.add(record.AccountId);
}
}
}
}
updateAccount();
}
----------------------
----------------------
1.3.2.d. The same test class is used to execute scenarios implemented with trigger context variables.
1.3.2.e. Execution Log
Recursion prevention with Static Set of Ids Recursion Prevention with Context Variables
There is only a slight difference in limit usage. However, by using context variables in our example
scenario, all necessary DML operations are performed, which were skipped in the first method that used
a static set of IDs.
In your use case, if there are implicit updates along with the main updates, a static set of IDs won’t be
effective. Additionally, updating fields not specified in the use case will still trigger execution, causing
unwanted DML operations and unnecessary limit consumption. Using trigger context variables helps avoid
these issues and prevents hitting governor limits.
Always, It is better to choose a recursion prevention method based on the Use Case
KARTHIKA . N www.linkedin.com/in/karthika-narayanan-282b866a