2

Let's say we are publishing platform events on Salesforce event bus. Some of the trigger points will provide an input of a List of records, some of the trigger points will only provide one record. Does it ever make sense to create a somewhat "duplicate" logic, but two routes, one for the processing of a single record, second for a processing of multiple records? For example:

public without sharing class ProcessingHandler {
    public static void processRecord(Case oneCase){
        // simplified logic
    }
    public static void processRecords(List<Case> moreCases){
        // bulkified logic
    }
}

The advantages:

  • Possibility to process a single record a little bit faster because a not bulkified route should execute a little bit faster (Maximum CPU time on the Salesforce servers).

The disadvantages:

  • Extra verbosity (Maximum number of characters for a class, Maximum amount of code used by all Apex code in an org).

I wanted to ask if my reasoning is correct and generally what is the standard on Salesforce platform?

2
  • 2
    It would never make sense to do this because you implement the same logic twice and must maintain both versions. Someone would need to ensure a bug in one must be fixed in the other. You may also find that behaviour then varies simply depending on the number of records (introducing inconsistency and confusion). In a non-bulk scenario you basically don't need to worry so much about performance overheads since you have all those limits available for handling one record, instead of n records. Always write bulkified code.
    – Phil W
    Nov 20, 2023 at 10:26
  • @PhilW Thanks a lot Phil!
    – DevelBase2
    Nov 20, 2023 at 10:44

4 Answers 4

6

No, it never makes sense to split your logic like that.

Code that is built to handle processing a collection of records is already capable of handling the case where there is only a single record.

About the only practical use for that pattern (two methods where one takes a single item and the other takes a collection) is to have the method for the single record put the single record into a collection and then call the version of the method that takes a collection (using polymorphism to provide a bit of convenience).

There is no measurable benefit1 to execution speed for a single item going through what I guess I'd call "singular logic" compared to it going through "bulk logic". Or rather, if there is, that means that you have so much else happening that you wouldn't be able to handle many/any records in bulk.

The standard on the SFDC platform is to work on collections of items, rather than a single item. You're not the only one on the instance/pod that you're using (unless you're among the largest of Salesforce's customers, who can consume an entire pod by themselves). The code that you run needs resources, and the other customers on your pod can't use those resources until your code finishes execution.

The governor limits help ensure equitable-ish access to resources on a pod, and thinking in terms of processing collections of records helps us stay within those governor limits. It helps to steer us towards things like "run a single query to grab all the data you need in one shot rather than running one query for each individual record".

1: And the first rule of optimization club is "define a measurable thing that you want to improve, then measure your existing logic to get a baseline"
4

Based on some simple benchmarks, the cost of looping over a collection consisting of one item is approximately 0.05ms, with index loops being slightly faster than for-each iterator loops. There's also a cost of about 8 bytes of heap used to establish either kind of loop. In a typical trigger consisting of two for loops, one to extract some kind of data, and the other to perform some kind of update, this represents a extra total execution time of 0.1ms.

In other words, there will never be a scenario where this "optimization" would outweigh all the disadvantages, including the extra maintenance time, having to edit two different code branches to fix a single problem or add a single feature, code coverage, the concerns you mentioned, and more. It is rare for duplicated code to be advantageous compared to using a for-each loop for a collection that has just one element. We're not writing video games where we need to count every millisecond, we're writing database code that needs to run "just good enough."

3

I like other answers provided, but I feel that one aspect is being ignored, that is the multiple signatures approach you can have while building your org's code.

There is really no sense in writing the whole execution paths for a single and for multiple records, as there are no clear benefits. However, for the purposes of writing reusable and more readable code, there is some sense in having multiple signatures for the same thing.

The idea is simple: the single-item signature is just going to call the multiple-item signature with a list of a single item. And this is something already implemented by the standard library, as seen in the Approval Class and Database Class methods.

As such, your methods would look like this:

public without sharing class ProcessingHandler {

    // note how it is now a one-liner
    public static void processRecord(Case record) {
        processRecords(new List<Case>{ record });
    }

    public static void processRecords(List<Case> records) {
        // bulkified logic
    }
}

With this, it is much better to write ProcessingHandler.processRecord(case) than ProcessingHandler.processRecord(new List<Case>{ case }) when processing a single record.

3

The gotcha here is that most business logic is defined (thought about, documented, tested) in terms of a single record and implementing for the bulk case often results in the business logic getting mixed up with the bulkification concerns e.g. collecting Id sets up front and caching of parent and child record maps. This separation of concerns failure makes implementation and maintenance much harder == more expensive.

Few (zero?) frameworks do it, but separating out the business logic (expressed on a single record basis) from the bulkification caching seems ideal to me. The bulkification caching needs to be notified in advance of the set of records and the parent and child relationships so it can query (lazily) in bulk, but then can call the single record business logic with methods to supply the needed parent and child objects. This seems fine on paper; interested if anyone has built this out?

(As a code base grows, the first governor limit I usually see hit is the 100 query limit in triggers. Often it is identical read-only queries repeated from different pieces of logic that contribute significantly to this problem. Caching is a big help there too.)

So the visible API works in bulk, but the implementation quickly moves into a framework that invokes single record business logic classes.

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .