1

I started working with Salesforce in 2015, and since then I've been told by mentors and the Salesforce community on the Internet that having multiple triggers for a single object is bad practice, mainly because the platform does not guarantee a trigger execution order. This makes absolute sense in a scenario where there is a single application being developed.

Considering a scenario where an enterprise customer wants to build different applications on their platform though, I think this recommendation doesn't make that much sense anymore. It is like having two installed AppExchange apps that have their own triggers on standard objects (like the Account).

So app A and B can have a trigger on an object, but as long as their logic are separate (logic from application A doesn't depend on logic from application B) there should be no issues. Does this make sense?

3
  • 1
    It still makes sense to keep to one trigger per object where it is possible to do so. Adding that layer of complexity to your investigative efforts can be a real nightmare.
    – Adrian Larson
    Commented Nov 12, 2019 at 17:53
  • My main concern is about dependency. When logic is centralized in a single trigger (which may or may not implement specific handlers for code readability), it also creates dependency on everything that is referenced by the code. If two separate apps implement different fields on the account object, and both fields show up on the trigger, then the fields are no longer specific to each package, but common to a "core" package. Seems worse than separating in two triggers, one for each app, to me. Commented Nov 12, 2019 at 17:56
  • 2
    Renato - in DF18, there was an interesting talk by John Daniel and advanced talk on how one uses the fflib pattern, dependency injection, and 2nd generation packaging to accomplish a one trigger per object but different apps exploiting the same logic
    – cropredy
    Commented Nov 12, 2019 at 20:20

2 Answers 2

3

I am convinced, that multiple triggers per object are a good practice. I fully disagree with the "one trigger per object" concept and believe, that it is outdated.

The paradigm originated way before 2nd generation packaging (SFDX). Back then, developers had one big unstructured pile of source (or were still developing on sandboxes ...). The considerations that were valid back then, do not apply anymore.

TL;DR

These are the reasons, why it is totally fine to have multiple triggers on the same object in an org:

  • You have multiple 2nd generation packages that implement their own domain layer (on the same objects).
  • Trigger frameworks introduce too much complexity while solving too few problems. They make maintenance harder, not easier.
  • You want to keep your triggers clean, but still want to execute certain logic only in certain contexts.

Thoughts About 2nd Generation Packages

The introduction of 2nd generation packaging made the "one trigger" paradigm obsolete. Your packages —by design— should not know about each other or declare a dependency hierarchy otherwise. In both cases, they cannot access or modify contents of the other package.

Without a heavy investment in a trigger framework (that both packages share as a dependency) it is technically not possible to have one trigger on the same object that both packages use.

The benefits of separating your code into packages (scaling development teams, independent deployments, lower complexity in projects, improved dependency management, etc) by far outweigh the negligible downsides of multiple triggers. You shouldn't have to worry about order of execution for functionalities that shouldn't depend on each other in the first place.

Until today, I did not find a framework out there that actually reduced complexity. All frameworks (including the ones I came up with) made trigger development much more tedious, introduced bizarre-to-implement interfaces, were hard to debug and less performant.

So to wrap it up:

  • If your packages/applications have domain logic on the same object, it is totally fine to implement multiple triggers per package.
  • Do not bother to introduce a framework. They all come with too much overhead for too little benefits.
  • It is much easier to design your domain logic so you don't rely on order of execution accross packages.
  • Treat your packages as independent applications: They should declare conscise "APIs" (if they are used as a dependency), advance with semantic versioning and should be independently deployable.

So you are totally fine, if you have one trigger per object per package. But not one per org any more (which was the original "best practice").

Thoughts About Reducing Complexity

Besides the package-based considerations, I like to keep the complexity of an application as minimal as possible. I design everything around the concept that the best code is code, that is never written. Less code means easier onboarding for new developers and less edge cases to handle.

I believe, that trigger frameworks and complex single-object triggers are diametrically opposed to this best practice. I see a very common misunderstanding within the developer community: A framework or library is not justified by its bare existence. It has to make a certain task so much easier (usually by abstraction or smart concepts), that the usage outweighs the additional complexity (understanding it, maintaining it, fixing it, relying on a third party, etc.)

Why I Don't Like Single-Object Triggers

Take this generic implementation that follows the "one trigger" paradigm, for example:

trigger onMyCustomObject on MyCustomObject__c(before insert, after insert, before update, after update, before delete, after delete, after undelete) {
    if (Trigger.isBefore) {
        if (Trigger.isInsert) {
            SomeHandler.specialBeforeInsertStuff(Trigger.new);
        }
        if (Trigger.isInsert || Trigger.isUpdate) {
            SomeHandler.functionalityOne(Trigger.new);
            SomeOtherHandler.functionalityTwo(Trigger.new, Trigger.oldMap);
        }
        if (Trigger.isUndelete) {
            SomeHandler.functionalityOne(Trigger.new);
        }
    } else {
        if (Trigger.isInsert) {
            SomeHandler.afterInsertUpdateStuff(Trigger.new);
        }
        if (Trigger.isDelete) {
            SomeHandler.calculateSomethingElse(Trigger.old);
        }
    }
}

The trigger executes on every DML statement, just to run logic to stop executing. This is completely unnecessary.

  • At least the context is solved within the trigger and not delegated to another class/framework.
  • Readability and overhead are manageable, but I think this approach lacks maintainability. It is not intuitive to see, what logic is executed in the insert context.
  • Adding new logic (e.g. adding a new execution context, if the trigger not already runs on every single context) may cause unintended execution of other handlers.

If you do not start with "all contexts", you will find yourself continuously adding contexts and if-else statements until you end up with a trigger like that.

Why I don't Like Trigger Frameworks

I evaluated a lot of frameworks (fflib, Dan Appleman's proposal, and virtually all proposals you can google) and found one common denominator among all of them: They introduce a lot of complexity and overhead, are hard to understand and basically solve no problem at all.

Let me emphasize: Order of execution shouldn't be a problem for you, if your domain layer is well designed. Solving design flaws in your code with massive overhead and complexity is an anti-pattern.

The frameworks I found can be divided in roughly two categories:

  • Custom metadata based frameworks that dynamically construct handlers based on a registry (these are compatible with 2nd generation packaging)
  • Monolithic "know it all" handlers that register all handlers at compile time in their "execute" method (these are not compatible with 2nd generation packaging)

Take this common "dynamic approach", for example:

trigger onMyCustomObject on MyCustomObject__c(before insert, after insert, before update, after update, before delete, after delete, after undelete) {
    MyTriggerFramework.execute(MyCustomObject__c.getSObjectType());
}

The framework itself is basically untestable. It makes heavy use of static Trigger.* variables to understand the context it is running in, constructing it's handlers with Type.forName and executing their onBeforeInsert, onAfterUpdate, etc methods).

  • The trigger must execute in all contexts, even though we might only have one handler registered for a specific context.
  • The framework must implement triggers on all standard objects (so packages that use the framework as a dependency can make use of it)
  • The framework does not know about custom objects, so the dependent packages must implement the "framework trigger" within their domain.
  • If something does not work as expected, you spend more time debugging if your handler ran in the wrong context, than actually debugging the implementation.
  • Debug logs are completely cluttered. Every DML statement (including your test fixtures) always executes all triggers/handlers, just to run through a bunch of if-else statements to determine, if the handler should be called or not.

A framework must therefore know about its dependents, which effectively breaks the idea of isolation.

This is a couple of hundred lines of debug log lines, just to understand that your code didn't even execute. Also, this code is slow. I frequently deal with debug logs that show idle/wait times of hundreds of ms until custom metadata is initialised (while SOQL gets executed in a tenth of the time ...). And as mentioned above: Registering those handlers to execute in a package-based environment is tedious to say the least.

Since monolithic frameworks are not compatible with second generation packaging it makes no sense to investigate them.

Taking It Further: One Trigger Per Context

What I find way more intuitive and much easier to scale is the following approach:

trigger onMyCustomObjectDelete on MyCustomObject__c(after delete) {
    MyCustomObjectDomain.doSomethingMeaningful1(Trigger.old);
    MyCustomObjectDomain.doSomethingEvenMoreImportant(Trigger.old);
}

trigger onMyCustomObjectInsert on MyCustomObject__c(before insert, after insert) {
    if (Trigger.isBefore) {
        MyCustomObjectDomain.fillMyFieldsFromSomething(Trigger.new);
        MyCustomObjectDomain.calculateFormulaFields(Trigger.new);
    } else {
        MyCustomObjectDomain.insertRelatedRecords(Trigger.new);
    }
}

trigger onMyCustomObjectUpdate on MyCustomObject__c(before update, after update) {
    if (Trigger.isBefore) {
        MyCustomObjectDomain.fillMyFieldsFromSomething(Trigger.new);
    }
}

Yes -- you end up with up to 4 triggers per object. But only, if you need to execute logic on all those contexts. In my opinion, the biggest advantage is the readability of code and debug logs as well as the testability of the code.

  • The implementation is usually very straight-forward and very easy to understand.
  • Salesforce handles the DML context for you. You don't have to. Your code only get's executed when it SHOULD, instead of executing always and then implementing a bunch of logic to stop the execution.
  • As a developer, I care to understand what happens when an object is inserted, updated or deleted. This allows me to see everything related to a particular transaction context in one file.
  • The code is very easy to extend without side effects. Need additional logic that is executed in before insert and before update context? Write the domain method and hook it into both triggers. No need to add additional if/else statements in the "single big trigger" to make sure existing code does not run in the new context (or, even worse, implement a handler with 7 interface methods where 5 methods do nothing and register it somewhere ...)

I know this might be a very controversial proposal. I would love to hear the communities thoughts about that.

1

It does indeed make sense if you can easily split the logic of the applications. But the question is why would you do it? That will require extra maintenance as the new requirements are coming in and you may end up with duplicate code which is a bad practice itself. You will also probably end up doing the same check(which application is running now and which trigger should be executed) multiple times instead of a simple if-else.

What I would strongly recommend is still 1 trigger per object that just calls a handler class and in this class you can split the logic into whatever you need.

For example:

public without sharing class TRIG_MyCustomOject_Handler implements ITriggerHandler {
    public void BeforeInsert(List<SObject> newItems) {
        List<MyCustomObject__c> scenarioOneList = new List<MyCustomObject__c>();
        List<MyCustomObject__c> scenarioTwoList = new List<MyCustomObject__c>();
        for (MyCustomObject__c mco : (List<MyCustomObject__c>)newItems) {
            if (mco.MyCustomBooleanField__c) {
                scenarioOneList.add(mco);
            } else {
                scenarioTwoList.add(mco);
            }
        }
        if (!scenarioOneList.isEmpty()) {
            // Call methods to do your scenario 1 logic
        }
        if (!scenarioTwoList.isEmpty()) {
            // Call methods to do your scenario 2 logic
        }
        // Call methods to do some generic logic if any
    }
}

I hope this makes sense

2
  • Why would I do this? To separate logic and to avoid the centralization of dependencies in a single package. The way you described, if the class implements fields from both applications, then the package depends on those two, but the logic should be owned by the package, not to depend on it. So my question is now regarding your answer: how does that work in a scenario where I have two applications with separate business logic to run on their triggers, on the same object? Commented Nov 12, 2019 at 17:12
  • 1
    @RenatoOliveira if you are developing managed packages it is indeed best to keep the logic apart from each other, you do not want to bring out a package with a billion dependencies. But when you write code in an org(SF apps are a part of an org), it is still recommended to only have one trigger.
    – Novarg
    Commented Nov 13, 2019 at 8:37

You must log in to answer this question.

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