11

Apex Design Patterns – Leveraging the flexibility of custom metadata types

Hello everyone, today I’d like to share how I’ve been leveraging custom metadata types in my Apex implementations and how it’s been improving my projects lately.

We all know that, no matter how well planned and executed a project is, there will always be changes that will need to be addressed after the code’s been completed. Depending on how your code is implemented and how complex is your application development cycle, those changes can take minutes or days. This is when custom metadata types come in handy and changes can be made on-the-fly and might not even be needed to be deployed across different environments.

Apex triggers

Either you’re working on a simple or complex project, chances are that there’ll be a requirement that will leverage an Apex trigger. It’s also not uncommon that the requirements related to a trigger are quite dynamic i.e. subject to change in unexpected ways due to changes within the business. We can prepare our trigger for those changes if we use custom metadata types.

Let’s jump right into an example. Imagine that we want to create a follow-up Task for every Opportunity inserted in our Org that is in the Qualification Stage and has its Amount greater than 10k. We can achieve this with the simple following code:

OpportunityTrigger.trigger

trigger OpportunityTrigger on Opportunity (after insert) {
    OpportunityTriggerHelper.createTask(Trigger.new);
}

OpportunityTriggerHelper.cls

public with sharing class OpportunityTriggerHelper {

    public static void createTask(List<Opportunity> opportunities) {
        List<Task> tasksToInsert = new List<Task>();
        for (Opportunity opportunity : opportunities) {
            if (opportunity.StageName == 'Qualification' && opportunity.Amount >= 10000) {
                Task task = new Task();
                task.Subject = 'Email';
                task.Description = 'Please follow-up with this Opportunity ASAP';
                task.WhatId = opportunity.Id;
                tasksToInsert.add(task);
            }
        }

        if (tasksToInsert.size() > 0) {
            insert tasksToInsert;
        }
    }
}

Now let’s take a step back and analyze this code. In terms of best practices we’re doing quite well since

  1. we’re bulkifying our code, which means that if we are inserting more than one Opportunity record at a time we should still stay within Salesforce’s limits,
  2. we’re also not including SOQL queries nor DML statements inside FOR loops, and
  3. we’re not including business logic directly inside the trigger file which makes future changes in the Opportunity trigger easier.

So, what’s the problem here? Well, in real-world projects with tight timelines, undecided clients, and ever-changing business requirements, changing the logic of a trigger, especially if the code’s already been deployed into production, can take days or even weeks depending on the complexity of the project development pipeline. How can we prepare our code for last-minute changes from the business?

Custom metadata types to the rescue

This is the moment where custom metadata types fit like a glove into our code. If you are working in a fast-paced project for a complex organization, and have seen requirements changing in unexpected ways before, or have a feeling that they might change soon in the future, I’d say that it’s worth putting a little additional effort implementing a small framework up-front that will make our code logic easier to adjust or extend later if needed. Let’s see how it would work.

For this example, we would need at least two new custom metadata types:

Trigger_Entry_Criteria_Set__mdt

Trigger Entry Criteria Set

The first custom metadata type that we need is a group of criteria, something that we’ll call as Trigger Entry Criteria Set. This type will simply work as a container for multiple criteria that are related to a particular trigger. For our example, it will have only two fields – a checkbox to populate the criteria set as active or not, and a text field that will be used as a reference for our helper class.

Trigger_Entry_Criterion__mdt

Trigger Entry Criterion

The second type that we will use in our example is the Trigger Entry Criterion. This type represents an individual criterion that is related to a specific criteria set. Its fields are an active flag, a comparison picklist which represents the operation related to a specific criterion (e.g. equals to, populated, greater than, etc.), the name and the value of the field related to the record that is being evaluated, and a lookup to a Trigger Entry Criteria Set record.

Custom Metadata Records

Custom Metadata Records

Having created the custom metadata types, we can start to insert some records in our example. To meet our defined requirements, we need one criteria set record, which will be called Qualified Opportunities, and two criterion records – one to match the Opportunity Stage, and another to match its Amount field. Let’s take a look at our new code for the trigger.

CriterionEvaluator.cls

public interface CriterionEvaluator {

    public Boolean evaluateCriterion(String fieldName, String fieldValue, SObject record);
}

The first new file that we will add to our code will be the CriterionEvaluator interface. This interface will be the base type for all comparison operators that we might want to use in our framework (e.g. equals to, greater than etc.). It has a single Boolean method to evaluate a particular criterion given a field name, field value, and a record to be evaluated.

EqualsCriterionEvaluator.cls

public with sharing class EqualsCriterionEvaluator implements CriterionEvaluator {

    public override Boolean evaluateCriterion(String fieldName, String fieldValue, SObject record) {
        String actualFieldValue = String.valueOf(record.get(fieldName));
        return actualFieldValue == fieldValue;
    }
}

We then have our first comparison operator for the Equals To comparison. It simply implements the evaluateCriterion method by retrieving the actual field value and comparing it with the expected field value defined in our custom metadata record.

GreaterThanEqualToCriterionEvaluator.cls

public with sharing class GreaterThanEqualToCriterionEvaluator implements CriterionEvaluator {

    public override Boolean evaluateCriterion(String fieldName, String fieldValue, SObject record) {
        String actualFieldValue = String.valueOf(record.get(fieldName));
        return Decimal.valueOf(actualFieldValue) >= Decimal.valueOf(fieldValue);
}

Our second comparison operator has a similar idea to the previous one. We retrieve the actual field value and then compare it with the expected field value. The only caveat is to properly cast the fields to the Decimal class before comparing them.

TriggerEntryCriteriaHelper.cls

public without sharing class TriggerEntryCriteriaHelper {
    private static Map<String, CriterionEvaluator> criterionEvaluatorImplementationByComparison = new Map<String, CriterionEvaluator> {
            'Equals' => new EqualsCriterionEvaluator(),
            'Greater than/Equal to' => new GreaterThanEqualToCriterionEvaluator(),
};

    public static Boolean meetRequirements(SObject record, String className) {
        Trigger_Entry_Criteria_Set__mdt criteriaSet = getEntryCriteriaSet(className);
        for (Trigger_Entry_Criterion__mdt criterion : criteriaSet.Trigger_Entry_Criteria__r) {
            CriterionEvaluator criterionEvaluator = criterionEvaluatorImplementationByComparison.get(criterion.Comparison__c);
            if (criterionEvaluator.evaluateCriterion(criterion.Field_Name__c, criterion.Field_Value__c, record) == false) {
                return false;
            }
        }

        return true;
    }

    private static Trigger_Entry_Criteria_Set__mdt getEntryCriteriaSet(String className) {
        return [
                SELECT Id,
                (
                        SELECT  Id,
                                Comparison__c,
                                Field_Name__c,
                                Field_Value__c
                        FROM Trigger_Entry_Criteria__r
                        WHERE Active__c = TRUE
                )
                FROM Trigger_Entry_Criteria_Set__mdt
                WHERE Helper_Class__c = :className
                AND Active__c = TRUE
        ];
    }
}

Next is TriggerEntryCriteriaHelper, the main class of our framework. This class has a Map with each one of our comparison evaluator types that can be easily extended if needed, one public method that decides if a particular record meets the requirements specified in the custom metadata, and an auxiliary method with a SOQL query to retrieve a Trigger Entry Criteria Set record and its related criterion records.

The meetRequirements method is straightforward – it retrieves the criteria records related to a given helper class, iterates over them returning false if at least one doesn’t pass, and then returns true otherwise i.e. all criteria are successfully satisfied.

Finally, we have a new version of our main trigger class:

OpportunityTriggerHelper.cls

public with sharing class OpportunityTriggerHelper {

    public static void createTask(List<Opportunity> opportunities) {
        List<Task> tasksToInsert = new List<Task>();
        for (Opportunity opportunity : opportunities) {
            if (TriggerEntryCriteriaHelper.meetRequirements(opportunity, 'QualifiedOpportunitiesHelper')) {
                Task task = new Task();
                task.Subject = 'Email';
                task.Description = 'Please follow-up with this Opportunity ASAP';
                task.WhatId = opportunity.Id;
                tasksToInsert.add(task);
            }
        }

        if (tasksToInsert.size() > 0) {
            insert tasksToInsert;
        }
    }
}

The only difference is the if statement, we’re not hard-coding it anymore but calling the TriggerEntryCriteriaHelper class passing the Opportunity record to be evaluated as well as the helper class name so we can fetch the related custom metadata records.

The main advantage of this alternate approach is that our trigger is now ready to accommodate any changes in its entry criteria since it’s all being driven by custom metadata. This means that the criteria can be changed by Salesforce Admins directly in the Production Org without the need of changing the code and then going through the whole application development cycle.

And that’s about it! Please let me know your thoughts about this framework in the comments. If you think that this type of content is useful and you’d like to see more of it, please like and share this post. As always, stay tuned for more Salesforce content!




Comments(11)

  1. Reply
    Chandra Viswanath says:

    Hi Jonatas,
    Thanks for a very useful and interesting solution to the flexibility requirements.
    I have one question here though, most solutions in real life have conflicting constraints (well that is true about life itself, but that is for another day :-)), so how do we compare this solution in terms of performance requirements vs the earlier case.
    In my pursuit of such solutions, I have often had to balance flexibility with how often that is really needed and keep on the lookout for alternatives by weighing them from business value. Of course, there is no perfect solution here. I would however, greatly appreciate your thoughts on a real life case where this level of flexibility was weighed over performance.
    Thanks so much for your time.

    • Reply
      Jonatas Barbosa says:

      Hi Chandra,

      Thanks for taking the time to go through the post and contribute with a comment, I really appreciate it. In terms of performance, I haven’t had a chance to compare both solutions through the use of the Apex benchmark methods (e.g. Limits.getgetCpuTime(), Limits.getQueries(), etc.). However, it’s clear that the second approach that uses custom metadata types is using more resources since it leverages the creation of a custom metadata type itself (Salesforce also has a limit related to that), the SOQL queries related to the retrieval of those records, and all the abstractions that need to be processed in order to execute the code logic.

      For your second point, I totally agree that this should not be the first approach to be taken in every case. I really believe that we should keep our solutions and implementations as simple as possible most of the time. However, if you have been working on a project that has demonstrated to have last-minute business changes on a certain frequency, I would definitely recommend the second approach instead.

      A simple heuristic that I like to use for myself is something that I like to call the “1, 2, N” approach. The idea is that we would do the straightforward solution for the first time, for the second time, and if a new case appears then it’s already a good signal that an abstraction is needed.

      I hope that those thoughts could help clarify the points that you have mentioned. Thanks again for your contribution!

  2. Reply
    Frederick Lane says:

    Is it possible to do the same using more declarative methods like Flow Designer? That way an Advanced Admin could do the lot. Also, how do we set up the testing of everything you’ve shown?

    • Reply
      Jonatas Barbosa says:

      Hi Frederick,

      Thanks for your comment! I believe you can do something similar for declarative solutions by leveraging custom settings. You can find an example of this approach here.

      For your second point, I’ll be providing the full code for this framework soon, including test classes, stay tuned!

  3. Reply
    Christian Schwabe says:

    Hi Jonatas,

    like the first reply to this article: Interesting approach to be as flexible as possible. Unfortunately there is no solution described related to the capabilities of apex tests for this highly flexible scenario. How does a test class with this solution looks like?

    Best regards,
    Christian

    • Reply
      Jonatas Barbosa says:

      Hi Christian,

      Thanks for your comment. I’ll be providing the complete source code for this framework soon, including its test classes. Stay tuned for more updates!

  4. Reply
    Nachiket Deshpande says:

    First of all , I really appreciate for this valuable information this is a great framework for trigger, but I think following things needs to be updated : –
    public interface CriterionEvaluator {

    public Boolean evaluateCriterion(String fieldName, String fieldValue, SObject record);
    }
    I think this should be a virtual class instead of interface
    public virtual with sharing class CriterionEvaluator
    {
    public virtual boolean evaluateCriterion(String fieldName, String fieldValue, SObject record)
    {
    return true;
    }
    }
    as we want to extend this class.
    and for criteriaSet.Trigger_Entry_Criteria__r it should be criteriaSet.ChildRelationShipName__r

    Please suggest and advise.
    Thanks,Nachiket

    • Reply
      Jonatas Barbosa says:

      Hi Nachiket,

      Thanks for taking the time to read the post and for your contribution. You’re right, I just updated the code to use the keyword “implements” instead of “extends” as we want to make sure our criteria classes implement the defined interface. It could be an abstract class as you mentioned, but usually, we use abstract classes when we have methods to be inherited by the child classes.

      In terms of the child relationship name, this is completely arbitrary so I decided to use Trigger_Entry_Criteria_Set__mdt.Trigger_Entry_Criteria__r in this example, but feel free to update it as you prefer.

      Thanks again for your contribution and let me know if you have additional questions or feedback!

  5. Reply
    Nachiket Deshpande says:

    I have one more question , how we can evaluate complex filter logic e.g (1 AND 2) OR (3 AND 4) I mean advanced filter logic ?

    • Reply
      Jonatas Barbosa says:

      That’s a great question, Nachiket!

      Stay tuned for more updates related to this framework, I’ll be releasing a new enhanced version soon!

  6. Reply
    Christian Schwabe says:

    Hi Jonatas,

    now I come to a point where I want to implement your really appreciated code to a real life scenario. Did you already publish a new enhanced version to support for instance an order of execution on different criterias and add AND or OR condition or for the test classes? 🙂

Post a comment