0% found this document useful (0 votes)
47 views19 pages

Explore Recursion With Scenario-1

Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
47 views19 pages

Explore Recursion With Scenario-1

Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 19

Recursive Trigger

1. Recursive Trigger
A recursive trigger in Salesforce is when a trigger accidentally calls itself, causing a loop of repeated
executions.

1.1. Error : “Maximum trigger depth exceeded”


Recursive triggers can lead to excessive trigger depth, causing errors like "Maximum trigger depth
exceeded". A trigger can only invoke other triggers 16 times in one transaction before it fails as per Governor
Limits.

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

ways depending on whether or not it fires triggers

1.1.a. Recursive Apex without firing triggers:

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.

1.1.b. Recursive Apex that fires triggers:

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.

1.2. Some of the Methods to avoid Recursive Trigger

1. Static Boolean Variable (Flag Method)


2. Static Set or Map for Record IDs
3. Trigger Context Variables
4. Using Future Methods or Queueable Apex
5. Framework-based Trigger Handlers (Trigger frameworks)
1.3. Implementation

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.

Let’s take three Scenarios :

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 obj = new copyPhoneHandler();


obj.doAction();

//CopyPhoneHandler
public class copyPhoneHandler
{
List<Account> triggerNewList;
List<Account> triggerOldList;
Map<Id,Account> triggerNewMap;
Map<Id,Account> triggerOldMap;

public static Set<Id> recursiveIds = new Set<Id>();


Set<Id> processIdSet = new Set<Id>();

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();
}
}
}
}

public void updatePhoneToContact()


{
List<Contact> toUpdateContact = new List<Contact>();

List<Contact> conList = [ Select Id, Phone, AccountId


From Contact
Where AccountId In :processIdSet
];

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)

ContactToCaseTriggerHandler obj = new ContactToCaseTriggerHandler();

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 obj = new CaseToAccountHandler();


obj.doAction();

// CaseToAccountHandler

public class CaseToAccountHandler


{
List<Case> triggerNewList;
List<Case> triggerOldList;
Map<Id,Case> triggerNewMap;
Map<Id,Case> triggerOldMap;

set<Id> processList = new Set<Id>();


public static Set<Id> recursiveIds = new Set<Id>();
public CaseToAccountHandler()
{
triggerNewList = (List<Case>)Trigger.New;
triggerOldList = (List<Case>)Trigger.Old;
triggerNewMap = (Map<Id,Case>)Trigger.NewMap;
triggerOldMap = (Map<Id,Case>)Trigger.OldMap;
}
public void doAction()
{
switch on Trigger.OperationType
{
When After_Update
{
for(Case record:triggerNewList)
{
if( !recursiveIds.contains(record.Id))
{

processList.add(record.AccountId);

recursiveIds.add(record.Id);
}
}

updateAccount();
}
}
}

public void updateAccount()


{
List<Account> accList = [Select Id, number_of_Open_high_priority_Cases__c,
Description,
(Select Id,Priority,Status,CaseNumber
From Cases
Where Status != 'Closed' and Priority = 'High')
From Account
Where Id In :processList];
for(Account acc:accList)
{
acc.number_of_Open_high_priority_Cases__c = acc.Cases.size();
for(Case rec:acc.Cases)
{
if(rec.status == 'Escalated')
acc.Description = 'Escalated : '+rec.CaseNumber;
}
}
if(!accList.isEmpty())
update accList;

//the below query is written to check whether description is added to Account or not

List<Account> acc = [Select Name, number_of_Open_high_priority_Cases__c,


Description From Account
Where Name Like 'RecursionTest%' ];
for(Account A:acc)
{
system.debug('Account Name : '+A.Name);

system.debug('Account Number of Open High priority cases :'


+A.number_of_Open_high_priority_Cases__c);
system.debug('Account Des : '+A.Description);

}
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;

List<Contact> conList = new List<Contact>();


for(Integer i = 0;i<2;i++)
{
Contact con = new Contact(LastName = 'RecursionTest Cont '+i,
Department = 'Finance',
AccountId = accList[0].Id);
conList.add(con);
}
insert conList;

List<Case> caseList = new List<Case>();

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';
}

List<Case> cList = [Select Id,Status,Subject


From Case
Where Subject Like 'RecursionTest Cont%'];
cList[0].Status = 'Escalated';

update accList; // *Update DML 1 – updating Account’s phone


update cList; // *Update DML 2 - updating a case status to ‘Escalated’

}
}
1.3.2.e. Execution Log

Let’s Check the Execution Log :

Yes! Recursion is Prevented.


Is that enough? Have you noticed the execution Log?
In the test class, code is written to insert Account records, and Contact records with the department

'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

executed, which is a drawback of this method.


1.3.3. Implementation With Recursion Prevention Method
Trigger Context Variables

1.3.3.a. Scenario 1
//CopyPhoneHandler ( Part of Coding with Changes)

public void doAction()


{
switch on Trigger.OperationType
{
When After_Update
{
for(Account accRecord:triggerNewList)
{
if(Trigger.isUpdate)
{
if(triggerOldMap.get(accRecord.Id).Phone !=
accRecord.Phone )
{

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();
}
----------
----------

public void updateCase()


{
List<Case> caseList = [ Select Id,Priority,ContactId
From Case
Where ContactId In :processIdSet
and Priority != 'High'
];
-----------
-----------
}
1.3.3.c. Scenario 3

//CaseToAccountHandler ( Part of Coding with Changes)

------------
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

Let’s Check the Execution Log :

Yes ! By this method also, Recursion is Prevented.

Ok, What about the *Update DML 2 ?

Yes, That’s also Done!


Let’s Check the Performance from the Execution Log.

1.4. Comparing the Limit Usage

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

You might also like