A Retrospective on using Apex or Flow to solve for the Same Business Scenario in Salesforce.
Author: Ismail Basser, Salesforce Consultant at Bluewave Technology
Table of Contents
Introduction
Coinciding with Dreamforce 2021, Salesforce announced the planned retirement of both Process Builder and Workflow Rules. As a replacement, Flow is the sole declarative option for all platform automation in the future. Subsequently, at TrailblazerDX 2022, there were a whopping 17 sessions dedicated to Flow, encouraging Salesforce practitioners to devote time to upskilling on this toolset. With Flow becoming more feature rich, its application also extends to scenarios previously reserved only for Apex. In this post, we will examine Flow against Apex for the same business scenario in order to draw some conclusions about the pros and cons of each option and where the line is in 2022.
The Scenario
A customer is already keeping track of their accounts and contacts in Salesforce. And they have a custom picklist, Level__c, on the contact to keep track of whether the contact has a primary, secondary or tertiary role to the related account. Maintaining such a contact role hierarchy helps this customer when determining which contacts to reach out to for account specific communications. While maintaining this custom picklist, the customer noticed a common pattern and subsequently have asked for our help to automate and save them time. Here’s the pattern they would like us to enforce:
When a non-primary contact is created Salesforce should set the same contacts “Reports To” field with the primary contact of the same account in order to maintain a hierarchy within the contacts related to that account. Essentially, we have one primary contact, at a time, per account for whom all other contacts should report.
After discussing this need a bit further, we have established some important clarifications and assumptions:
- We will use the existing custom Level__c picklist with values ‘Primary, Secondary, Tertiary’.
- We will use the existing standard lookup on contact, “Reports To,” which is a self-referential lookup to contact.
- An account can be related to many contacts but only one contact will be the primary contact for that account.
- There is already automation in their Salesforce Org to ensure only one contact may be set ‘primary’ at a time.
- When a contact record is created with either the ‘secondary’ or ‘tertiary’ Level__c value and no primary contact record exists associated to the related account, the contact being created will be updated to the primary contact for the account.
- All contacts with either the ‘secondary’ or ‘tertiary’ Level__c value should have the “Reports To” field updated with the Contact Id of the primary contact of the related account.
Delivering a Flow Solution
Before we can get started, we need to agree the triggering event for this automation request, that is, when to intervene in the database order of execution. As you may know, there is a growing list of Flow Types from screen flows, scheduled triggered flows, auto-launched flows, to record triggered flows and platform event triggered flows. Because we want our rule enforced on the contact at the time of record creation, we can use a record triggered flow. This Flow Type enables us to execute our logic behind the scenes before the new contact record is inserted into the database.
Below pictured is the completed record triggered Flow. Don’t worry, we will step through it element by element.
Getting Started
After choosing a Flow Type, our next critical decision is determining how to optimise the Flow, in this case either for ‘Fast Field Updates’ or ‘Actions and Related Records.’ These optimisation choices correspond to before or after save events respectively in the order of execution. By choosing ‘Fast Field Updates,’ we ensure our logic is evaluated before the record is saved in the database. The reason why a ‘Fast Field Update’ or the equivalent before insert/update trigger is the best approach here is because the logic is executed on the same record that fires the automation as opposed to any related records.
Let’s configure the Flow start screen using the following values, like so:
Attribute | Set As |
Object | Contact |
Configure Trigger | Trigger the flow when a record is created |
Entry Criteria | Level__c field on Contact has the value “Secondary” or “Tertiary” |
Optimise the flow for | Fast Field Updates |
Node 1/4 The Get Records Element
The Get Records element is used to “Find Salesforce records that meet filter conditions, and store values from the records in variables.” We want to use this element to query the database in search of a Contact record related to the same Account with the Level__c value “Primary.” This related Contact record will then be used to set the “Reports To” field on the original Contact record that started this flow. The variable ‘$Record’ in the screenshot below refers to the global variable already defined as the record that triggered this Flow and is available to use without any setup/configuration of the variable.
We will only store one resulting Contact record as there can only be one “Primary” Contact for each Account.
Node 2/4 The Decision Element
Next, the Decision element is used to “Evaluate a set of conditions, and route users through the flow based on the outcomes of those conditions..” Our decision will answer a simple question, “did we find a primary contact associated to the account?,” with two possible outcomes. If our Get Records element “Get_Primary_Contact” returned a Contact record then the outcome will be yes, otherwise the outcome will be no.
The “No” outcome is the default decision without any conditions.
Node 3/4 The ‘Reports To’ Assignment Element
The Assignment element is used to “Set values in variables, collection variables, record variables, record collection variables, and global variables.” When the outcome of our Decision element is “Yes” then we want to set the “Reports To” (Contact Lookup) field value on the Contact record that has been created to the Contact Id returned from the “Get_Primary_Contact” Get Records element.
Node 4/4: The ‘Level’ Assignment Element
Finally, we want to use a second Assignment element to handle when the outcome of the Decision element is “No.” Effectively, if no primary contact record is returned from our Get Records element then we will set the Level__c field on the Contact record that has been created to ‘Primary’.
Delivering an Apex Solution
The Trigger
First, we need to create a Apex Trigger on the Contact object, if it doesn’t have one, and equivalent starting point to specifying the Flows Type and Optimization strategy. Object oriented programming best practice promotes a single trigger per object avoiding any application logic directly in the trigger itself by using a handler class instead. For this example, we will establish a context condition to execute a new method however in a real-world scenario there might be further steps required to adhere to those best practices.
trigger ContactTrigger on Contact (before insert) {
if(trigger.isbefore && trigger.isInsert)
{
ContactHelper.setReportsToAsPrimaryContact(trigger.new);
}
}
The Class
When a Contact record is inserted in the database, our new Apex method “setReportsToAsPrimaryContact” will be executed, passing a list of contact records in the context of trigger.new for our method to evaluate.
Essentially, the same functionality as we developed in the Flow will now run in this Apex class; For every Contact record that is inserted, the following logic finds the ‘Primary’ Contact record for the related Account. Then this ‘Primary’ Contact is stored in a map and retrieved from the map using the accountId as a key to set the relevant Contact record value.
There is additional logic required in the Apex Class to handle bulk scenarios where multiple ‘non-primary’ Contacts, related to the same Account, being inserted and a ‘Primary’ Contact record does not exist. When this occurs, one of the Contact records must be chosen to be set as the ‘Primary’ Contact record.
While the resulting Apex method is simple enough, it requires an Apex developer to understand collections such as sets, lists and maps to be successful. Furthermore, they would surely follow coding standards like avoiding the use of DML or SOQL inside of For Loops.
public class ContactHelper {
public static void setReportsToAsPrimaryContact(List<Contact> newContacts)
//create set of account ids related to contact
Set<Id> accountIds = new Set<Id>();
for(Contact contact : newContacts)
{
if(!accountIds.contains(contact.accountId))
{
accountIds.add(contact.accountId);
}
}
//get primary contacts for each account
List<Contact> primaryContactList = [SELECT Id, accountId FROM Contact WHERE AccountId IN:accountIds AND Level__c = 'Primary'];
Map<Id, Contact> primaryContactByAccountId = new Map<Id, Contact>();
for(Contact primaryContact : primaryContactList)
{
primaryContactByAccountId.put(primaryContact.accountId, primaryContact);
}
//get primary contacts for each account
Map<Id, List<Contact>> nonPrimaryContactByAccountId = new Map<Id, List<Contact>>();
for(Contact nonPrimaryContact : newContacts)
{
if(!nonPrimaryContactByAccountId.containsKey(nonPrimaryContact.accountId))
{
nonPrimaryContactByAccountId.put(nonPrimaryContact.accountId, new List<Contact>{nonPrimaryContact});
}
else
{
nonPrimaryContactByAccountId.get(nonPrimaryContact.accountId).add(nonPrimaryContact);
}
}
for(Contact updateContact : newContacts)
{
if(updateContact.Level__c != 'Primary')
{
Contact primaryContact = primaryContactByAccountId.get(updateContact.accountId);
if(primaryContact != null)
{
//set the reports to id for the new contacts created to the primary contact if one exists
updateContact.ReportsToId = primaryContact.Id;
}
else
{
//if no primary contacts exists for the account then set the current contact as the primary
//if there are multiple contacts being inserted related to the same account then get the first contact from the insert list
//and set that contact as the primary one
List<Contact> nonPrimaryContacts = nonPrimaryContactByAccountId.get(updateContact.accountId);
if(nonPrimaryContacts.size() > 1)
{
nonPrimaryContacts[0].Level__c = 'Primary';
}
else
{
updateContact.Level__c = 'Primary';
}
}
}
}
//no update dml required as this will be called from a before insert trigger
}
}
The Unit Tests
To promote Apex to production, any snippet of Apex must have quality unit tests covering a minimum of 75% of the logic therein. This is a hard requirement by Salesforce. And it often takes a similar amount of time to author unit tests in a Test Class as it takes a programmatic developer to write the logic in an Apex Class. In fact, many developers prefer to write the unit tests first as a means of confirming the testable conditions and outcomes of the logic. Unit Tests will cover positive, negative and bulk processing scenarios for the associated logic. And these unit tests must be maintained along with the application logic when business processes are added, changed, or deprecated in the future.
Conclusion
Applying the problem-solving principle, “The simplest solution is always the best,” going forward if we can use Flow over Apex, we should. And due to substantial increases in the abilities of Flow over the last few release cycles, there is an ever growing list of use cases where Apex is no longer required. Where we, as Salesforce practitioners, need to use caution is in our own bias. Declarative (that is point-and-click) developers need to understand the limitations of Flow just as thoroughly as Programmatic Developers to avoid forcing the wrong solution forward simply because it was the toolset they were most familiar with. Without a doubt, there will always be a need for Apex on the Salesforce Platform but with the expansion of the Flow capability set, it means Programmatic Developers can refocus on more complex challenges. Below is a recap of pros and cons from the above business scenario.
PROS of a Flow Solution
- No programming means less time to delivery and reduced long-term maintenance effort
- A Flow solution doesn’t require programming skills to build, test, implement or maintain.
- Record Triggered Fast Field Updates actually use less CPU than Before Insert Update Apex.
CONS of a Flow Solution
- A Flow solution can either process records synchronously or asynchronously but not both. In this way, we are somewhat limited in our ability to address LDVs.
- A Flow solution is fundamentally less scalable than Apex, this is the trade-off for simplicity, and so you may bump into limitations as business processes become more complex.
- While debugging Flows is possible, it is not as robust as Apex where system.debug() can be added after any line in the code to facilitate troubleshooting.
PROS of an Apex Solution
- An Apex solution is scalable to handle all forms of bulk processing (including the option to process records asynchronously).
- Apex has very robust methods for troubleshooting application logic and unit tests.
- Apex offers more possibilities for further enhancements.
CONS of an Apex Solution
- An Apex solution generally takes more time to deliver and maintain than the equivalent Flow solution.
- An Apex solution requires programming skills to build, test, implement and maintain.
- An Apex solution requires additional logic and unit tests to be written to cover bulk scenarios.
If you enjoyed this post, may we suggest:
Don’t hesitate to enquire how Bluewave Technology can assist with any challenges you are facing within your Salesforce environment. We are Salesforce experts with two decades of success supporting Pardot, Sales Cloud, Service Cloud, Analytics Cloud, Experience Cloud, Marketing Cloud, Project Management, Integration, and Release Management.