8

I'm all too familiar with the Kevin O'Hara trigger framework and have used it as a base framework in many projects before.

However, I've recently been doing some in-depth reading about the idea of loose coupling between triggers and handlers, and the use of dynamic metadata based handler frameworks to control which code runs in which context and in what order. The two most common frameworks of this design that come to mind are Apex Trigger Actions Framework and Nebula Trigger Framework

As I look for a new framework to help elevate the large, seasoned org I've adopted, I'm finding myself at an impasse where it's no longer clear to me which is more acceptable as the current standard, or whether or not each approach is still acceptable.

I'm familiar with the "old" way of doing things, where a single Object Trigger calls a method on an ObjectTriggerHandler which extends the TriggerHandler base class, and then runs through each trigger context to call the related context method in the Object Handler. Each context method then does (typically) one large loop through all of the records in the Trigger to identify records that need to have actions taken against them, and then takes those actions after finalizing the loop (usually by passing data to other Utility classes to run reusable logic).

The new dynamic custom metadata style instead appears to have a single Object Trigger and multiple purpose built Object-specific classes which are queried for and then called in pre-arranged order by the MetadataTriggerManager. By leveraging interfaces and custom fields on the custom metadata records, the framework is able to be decoupled from the logic classes themselves, allowing for independent deployment of new logic classes without needing to change core framework classes or other classes outside of your project scope.

While I can see definite value (working with unlocked packages, better dependency management), I still find myself stuck in the middle. Some of my immediate observations of this style were:

  1. The entire contents of Trigger.new are passed to each method setup to run in that particular context, and so each method must loop through the entire list of records again. This (to me) seems like a lot of redundant processing of the trigger.new list multiple times that we could have processed once and split out into smaller working lists. Am I off base with this concern?

  2. Consider a design where you have 4 separate classes that implement the AfterUpdate interface on the Account object. 2 of those classes both want to make DML against existing Contacts that meet certain conditions, so they each loop the Trigger.new list, Query for related Contacts, make their changes in memory, and then run DML. Since each method is uncoupled, this would result in 2 SOQL queries and 2 DMLs for the same Object type (Contact). I could see this ballooning out of control in some complex situations.

Ultimately, I think my concrete questions are:

  1. Do older Trigger Handler frameworks still have a legitimate place in the future of SFDC apex framework architecture?
  2. Do these newer frameworks somehow account for my duplicate SOQL/DML concerns in a way that I haven't understood?
  3. If implementing a brand new framework into an existing large organization, is metadata driven frameworks the correct way to go, or is this still up for personal debate between professionals?
5
  • 2
    have you looked at Apex Enterprise Patterns (aka fflib) github.com/apex-enterprise-patterns along with force-di (part of above) and at4dx (part of above)?
    – cropredy
    Jun 16, 2021 at 21:35
  • No time for a full answer now for me, but on your first concern (and I acknowledge we are talking Apex here where the performance landscape is a bit different) coding/OO best practice is to ensure that each part of your code has a single responsibility, and that extends to for loops. A loop that performs multiple different tasks should be split into multiple loops all doing a single task. This helps keep code clean and reduces complexity.
    – Phil W
    Jun 16, 2021 at 22:33
  • On the use of metadata based configuration and dynamic class instantiation, I think this has a number of real benefits, but you are right that it can lead to governor limit issues. It can also mean, if you don't package protected metadata records, that an implementation could inappropriately change the config and completely break your processing. (Talking from the perspective of an ISV)
    – Phil W
    Jun 16, 2021 at 22:37
  • Hi Phil! While I agree with you about coding/OO best practices, I've always been more cautious when working in Apex due to the governor limits of SFDC. Specifically, I tend to do one large trigger.new loop in afterInsert for example, and during the course of that loop I identify records of varying criteria which are placed in smaller purpose built lists, and then those smaller lists are sent to separate parts of code following single responsibility principles (just on smaller data sets). I do this to avoid looping trigger.new over and over and over again. Is this overkill? Jun 17, 2021 at 2:06
  • 2
    Aidan Harding has some interesting thoughts about this here: nebulaconsulting.co.uk/insights/…
    – cropredy
    Jun 17, 2021 at 5:47

1 Answer 1

4

As the author of the Nebula Trigger Framework, I have opinions!

I think that the old style of trigger frameworks should stay in the past. Consistency within an org counts for a lot but, all other things being equal either, either of those frameworks is better than the old ways.

Style

I believe that a framework should encourage good style. It should be just opinionated enough to cause you to naturally fall towards good practice, whilst also allowing a plurality of styles. And, for the times when ugly is the only way, that should be possible too.

Decomposing triggers into separate classes encourages good practice. With older trigger frameworks, adding code in the most obvious and lazy way results in one big handler class.

Either framework can be shoehorned into working the other way round. But having small components is pretty well accepted as being a good engineering practice. And I'd go for the framework which starts from a good place.

Deployment

You really need something like metadata-driven triggers to work with Unlocked Packages. Even without Unlocked Packages, we still want to work as a coding team. When the control over what trigger code runs is distributed, teamwork is easier.

A diff showing just additions is easier to deal with than one that also has modifications. And there will be fewer merge conflicts in source control with the new trigger framework.

Efficiency

We all know that premature optimisation is the root of all evil. Most of the time, the performance cost of looping over the trigger context a few times is fairly small. For many systems, we can just ignore that cost because other governor limits dominate first.

In general, I'd always choose clarity over efficiency until something forces that to change.

Even when performance is important, I don't think that the trigger framework is the place to solve it. The trigger framework should just be about coordinating what code runs when on a database change.

We can always use components that are shared between trigger handlers in order to push some of the functionality back together. A well-proven example of this is the Unit of Work from FFLIB. You can have many separate trigger handler classes, all sending their DML requests to a single unit of work, to be run efficiently together.

Similarly, you could roll your own way of sharing DML, or the trigger context if you needed to.

Long-term, though, more Apex will be event-driven and/or running off-platform in Functions or whatever. So CPU limits are likely to become less relevant.

2
  • 2
    Thanks for your insight. I guess I just don't understand how a framework like this accounts for the real possibility of multiple classes needing the same relationship data and burning through multiple SOQL queries to get the data for example. Lets say on the Event trigger I have two classes that serve different purposes but both need related Contact data. Each of those classes would individually burn SOQL and SOQL Query Row Limits to get the related Contact data, instead of a single Contact data query in a larger handler on all WhoIds of the Contact type. How is that better? Jun 17, 2021 at 13:36
  • My point is that the trigger framework doesn't account for consolidating the queries in that scenario. I don't think that it should. It's a separate concern. Most of the time, you won't need to consolidate, so the extra code complexity would be wasted. And when you do, you can build layers on top to solve it.
    – Aidan
    Jun 18, 2021 at 8:45

You must log in to answer this question.

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