4

This question was asked similarly last month in Break single responsibility to save a soql call and was closed for being opinion based. The given use case also involved Queueable and so wouldn't likely be faced with the same governor risk as a synchronous transaction.

However, as a question in general, I disagree that it's opinion based, so I'm going to try asking it again.

The Single Responsibility principle tells us that a class or method should only have one reason to change, and only one responsibility. Taking it a step further and looking at "Traditional" Trigger Frameworks vs Dynamic Metadata Trigger Frameworks, we can see that the more modern metadata trigger frameworks are following a decoupled single responsibility structure, where new features & functionality are introduced as new classes which run independently and are called dynamically from the trigger framework.

The most common governor limit challenges I can imagine in this structure are SOQL and Heap. Heap is manageable to a degree by collecting your own garbage after you are done working with data, but 100 SOQL queries is a hard limit.

I know that pre-mature optimization is the root of all evil, and I agree - but in the environment I've adopted there is a real reason to be concerned that we may have enough cascading apex workflows to brush by that limit.

Take for example the following Metadata trigger structure:

Event Trigger Metadata Trigger Structure Diagram

In this workflow, EventTrigger is the sole trigger firing in all contexts. It calls the MetadataTriggerHandler which pulls the metadata records from Metadata_Object__c to understand what to run for this trigger, and then runs them in a predefined order in the correct contexts. Both of the classes, EventCreateAttendees and EventCreateAffiliates have a single responsibility of creating different SObject Types off the back of the Event insertion. However, both are reliant on Contact data pulled through the parent relationship query of WhoId. Both need different fields returned in their queries.

In this design, we will consume 2 SOQL queries to collect all of this different information. The singleton pattern of creating a class/method to run and cache this query in a static variable doesn't work either because both EventCreateAttendees and EventCreateAffiliates might be querying for different Contact Ids based on Event filtering conditions defined in their codebase.

If we break the single responsibility pattern, we could create a single EventCreateRelatedRecords class which loops through the full Trigger.new to find all Contact Ids at once, does one larger SOQL query for all of the combined fields for all matched Contacts, and then uses the fields appropriately to create both Attendees and Affiliates.

So I guess, ultimately, my longwinded question is...

Do we as architects just decide which governor limits we want to battle against more often and pick a design that fits 80% of our use cases? Do we break the generally accepted SOLID principles when necessary to solve for governor limits? Or is there an acceptable approach that solves for this type of issue while still being considered best practice and aligned with SOLID?

4

SOLID is something that tends to happen when you write good code, but good code is not necessarily something that happens when you implement SOLID. In other words, SOLID is a description of what good code looks like, not a prescription for getting good code in the first place.

I can, for example, write very SOLID code that will crash in any Salesforce org because of governor limits. I can also write SOLID code that will run better than many junior developers' unshaped and non-SOLID code. Being SOLID is no guarantee of performance, and implemented poorly, may end up being a worse choice than simply writing spaghetti code.

SOLID is a set of design principles that aid in readability and maintainability of code. It does absolutely nothing to optimize performance or make code that is actually useful for end users. In other words, SOLID is a goal you should strive for, not a rule you must die by.


If we break the single responsibility pattern, we could create a single EventCreateRelatedRecords class which loops through the full Trigger.new to find all Contact Ids at once, does one larger SOQL query for all of the combined fields for all matched Contacts, and then uses the fields appropriately to create both Attendees and Affiliates.

This here is where your problem is. The Single Responsibility Principle does not state that each class needs to be a single line of code or something like that, or literally do only one thing at a logical level. I saw a question elsewhere where someone was asking if a method was allowed to use a logger to write logs, or if that would violate the SRP. The answer, of course, is that as long as the logger is implemented in a different class, it's totally fine. The logger has its own responsibility, and the method did what it did, including logging. The SRP boundary needs to be broad enough to make sense.

Let's talk this through a real-life example. Would you rather (a) go to the grocery store, go home, go to the hardware store, go home, and then go to the library and go home, or (b) go the the library, the hardware store, then the grocery store, then go home? Unless there were other constraints, it would clearly save money and time to make one long trip.

Similarly, if the only good reason you have for multiple queries is because of SOLID, then you've misunderstood the point of the principles. Following SOLID too literally deoptimizes your code in favor of maintainability. You always need to balance maintainability and performance, and not sacrifice too far either direction.

To apply this to the above scenario, that means that you would ideally have two, or even three, classes (presuming you did the split for Unlocked Packages, etc). The first falls in the Selector class (from Domain and Selector Layers), which would provide a common selector for both functions that need this related data, while the other classes would be Domain classes that would be called to perform the relevant updates (using the Selector as the source of data).

Do we as architects just decide which governor limits we want to battle against more often and pick a design that fits 80% of our use cases?

There's no such thing as code that supports 80% of your use cases. It's better to be 100% usable and 80% SOLID than 100% SOLID and 80% usable. We do have worry about the trifecta of resources: memory, processing time, and code complexity. Every change to optimize one affects the other two as well.

For example, combining the queries from each trigger handler results in more memory use, less processing time, and potentially more code complexity. You can tug at each one of these individually, but they will affect the other two. Generally speaking, you can optimize for two of the three, but never for all three; the third is necessarily forced by the choice of the other two.

Do we break the generally accepted SOLID principles when necessary to solve for governor limits?

Yes. As I stated, SOLID is useless if nobody can use your code, no matter how maintainable it is. Your users don't care that you can add a feature three times faster if it also means the system is three times slower. And, as I started off with, SOLID is merely a description of what your code should look like when you're done, not a prescription that demands your code look a certain way.

If your code reaches a point of uselessness, you've either misunderstood SOLID in general, or implemented SOLID at too granular a level. SOLID should, at no point, cause your code to become inoperable or insufferable for your end users.

Or is there an acceptable approach that solves for this type of issue while still being considered best practice and aligned with SOLID?

I linked to this above; the Domain and Service Layers are strongly recommended. If implemented correctly, you'll save a bundle on queries while still being decidedly in the SOLID territory. Just keep in mind that your need to be SOLID should never overwhelm your need to be performant.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.