Steps


Concept

In general, we can say that functionalities that we are implementing are executed in order to handle actions or events like button clicks on the UI, salesforce triggers or API calls. In Salesforce each event is handled in a separate transaction and each transaction is limited by transactional and platform limits (Apex Governor Limits). Each functionality costs because it always consumes CPU time and other Salesforce resources. Whenever more functionalities are trying to run the more resources are consumed which may result in transaction overloading, highly unpredictable behaviors, and data losses. Below image presents the transaction overload generated by adding new functionalities.

Transaction Overload Schema Transaction Overload Schema

As developers we have a couple of ways to reduce Salesforce limits consumption:
asynchronous apex execution - but such approach also has its own limitations.
reduction of data loading and data saving operations which are the most time-consuming processes.

Here comes Breezz that introduces special interfaces which allow us to build functionalities in a very optimized way. The diagram below presents differences between standard development approach and the Breezz way.

Step Approach Step Approach

StepGroupHandler - Breezz allows to create a group of Steps and StepGroupHandler responsible for entire process execution. StepFunctionGroup creates DataStore and ModificationContext and shares them with all Steps - in this way all Steps in the Group can access the same data and modify the same records that allows to reduce the number of SOQLs and DMLs.

Step - is a unit of work, the atomic functionality which we need to execute. Each Step in the Group uses the same instance of DataStore and ModificationContext. Step interface contains below 5 methods that allow to implement almost any simple complex logic:

Step Schema Step Schema

Step methods are executed in the order stated below:

  1. void initialize() - is used for additional Step level initialization (for example loading the configuration form custom metadata), GroupHandler is executing initialize method once for each Step in configured order (based on the priority field in the Step Configuration)

  2. Boolean initRecordProcessing(Object record, Object optionalOldRecord) - GroupHandler is executing initRecordProcessing method for each processed record (for example trigger context) and each Step in configured order (based on the priority field in the Step Configuration). Should be used to modify record fields if all required data are available. ModificationContext should be used to modify related data. DML and Queries are not allowed in this method. DataStore should be used to load data. If the method returns true then the finishRecordProcessing method will be executed for the newRecord record.

  3. void finishRecordProcessing(Object record, Object optionalOldRecord) - Method is executed for records for which initRecordProcessing method returns true. It should be used to modify record fields if all required data are available or use the ModificationContext to modify related data. DML and Queries are not allowed in this methods. Between initRecordProcessing and finishRecordProcessing Step Group Handler uses Loaders to load data into the DataStore.

  4. void finishSyncProcess(List records, List optionalOldRecords) - Finalizes sync process. The records and optionalOldRecords parameters will contain records added by addToSyncFinish method (method could be used in initRecordProcessing and finishRecordProcessing). This method will always run even if the records list is empty.

  5. void executeAsyncProcess(Map<String, forvendi.AsyncJobInfo> asyncJobsByRecordKey) - runs for all records added by addAsyncJob method (method could be used in initRecordProcessing, finishRecordProcessing or finishSyncProcess)

DataLoader - Responsible for loading data. Custom loaders need to implement forvendi.DataStore.Loader interface and need to be registered in the **StepConfiguration ** or StepGroupConfiguration.

DataStore - request for data and store them after loading. DataStore uses Loaders to load records. DataStore is able to load flat SObject records based on the Ids without registering custom Loaders (Generic Loaders).

ModificationContext - ModificationContext gathers and stores SObject records to insert, update or delete.

Breezz framework allows running StepGroups from Salesforce Triggers, Salesforce Flows or directly from Apex code.


Good Practices

To ensure the effective implementation of Breezz in an SF application, the following guidelines should be followed:

  • For solutions implemented in Apex code, every method invoked within Apex Triggers requires the introduction of a new class that extends the forvendi.Step abstract class. This extended class provides the essential Breezz methods required to establish a record processing flow.
  • Steps handling ‘before’ triggers should solely modify records from the trigger context.
  • Steps handling ‘after’ triggers should solely modify records other than the trigger context.
  • Asynchronous calls in ‘before’ triggers are not optimal.
  • Step methods guide:
    1. void Initialize()
      • should be overridden when additional level initialization for the step is required
    2. Boolean initRecordProcessing(Object record, Object optionalOldRecord)
      • should be used to modify record fields if all the necessary data is available straight away,
      • neither DML, SOQL nor SOSL are allowed here,
      • in order to modify related records ModificationContext has to be used
        • ModificationContext gathers and stores SObject records to insert, update or delete,
        • Can be called with getContext() method from forvendi.Step,
      • Full list of methods available here
        Be careful to use the methods correctly

        There is a difference between addToUpdate(SObject record) and addModificationToUpdate(SObject record, SObjectField objectField, Object value). The first method adds the given record to the list of records to be updated in the database, while the second adds a single field modification to the record to be updated. This means that if you want to add several modifications, you should use the latter, because the former would swap the entire record instead of expanding it with subsequent changes

      • In order to query data from the database DataStore has to be used.
        • DataStore requests for data and stores them after loading.
        • Can be called with getStore() method from forvendi.Step.
        • Full list of methods available here.
        • Without registering custom Loaders DataStore is able to load flat SObject records based on the Ids. It can be achieved with requestToLoad(List<Id> ids, Set<String> fields). Inner queries can be used as well, ex:
          getStore().requestToLoad(accountRecord.Id, new Set<String> {'Id', 'OwnerId', '(SELECT Id FROM Public_Accounts__r)'})
          
        • If there is a need for a more advanced query with different clauses custom loader has to be defined first in Breezz configuration. In such scenario, requestToLoad requires also storeKey attribute which is the name of the custom loader,
        • In this method you can not use the data requested from DataStore yet.
      • returns Boolean:
        • Should be false if all the record processing could be done based on data available in the trigger without a need for additional queries.
        • However, if data required for additional processing has been requested using any of the DataStore methods, ’true’ must be returned. This is because, for the records where ‘initRecordProcessing’ returns ’true,’ Breezz proceeds to execute ‘finishRecordProcessing’ and that is the place where the requested data becomes accessible at last.
    3. void finishRecordProcessing(Object record, Object optionalOldRecord)
      • Is executed for records for which initRecordProcessing returns true.
      • DML and queries are also not allowed here
      • Since data requested to load in the previous method is loaded between initRecordProcessing and finishRecordProcessing, it can be accessed here with getStore().getFromStore(recordId) which returns SObject if it does not come from custom Loader or Map otherwise
      • Like in initRecordProcessing records fields can be modified here and related records can be modified/created using ModificationContext
    4. void finishSyncProcess(List records, List optionalOldRecords)
      • Finalizes the sync process. The records and optionalOldRecords parameters will contain records added by addToSyncFinish method which can be used in initRecordProcessing and finishRecordProcessing.
      • Runs always even if no records have been added to sync.
    5. void executeAsyncProcess(Map<String, forvendi.AsyncJobInfo> asyncJobsByRecordKey)
      • Runs for all records added by addAsyncJob method which can be called in initRecordProcessing, finishRecordProcessing or finishSyncProcess
  • Flow Interviews can be skipped in Apex Steps implementation and replaced with flow steps configured in Breezz with an assumption that the given flow has no input parameters coming from apex logic
  • Useful ModificationContext methods are as follows:
    • Sending EmailMessages should be done with the use of getContext().addToSend(Messaging.SingleEmailMessage record)
    • Records manipulations can be also done asynchronously and are covered by methods like addToAsyncInsert, addToAsyncUpdate, addToAsyncRemove and others
    • Platform Events should be published with the use of addEventToPublish
    • Leads conversion can be done with the use of addToConvert
    • getters like getRecordsToUpdate, getEventToPublish, getRecordsToConvert, and others allow checking what exact records await processing
  • Breezz Logger -forvendi.BreezzApi.LOGGER.logError() method allows to log code execution warnings and errors and gathers execution metrics