30 Sep 2021
Salesforce’s Nonprofit Success Pack (“NPSP”) and Education Data Architecture (“EDA”) installable packages both come with functionality called Table-Driven Trigger Management (“TDTM”).
The admin options are powerful enough to break dev code, so good practices around handoff are essential.
Trigger developers working in an org with one of these packages installed are encouraged to break their code up into “trigger handler” classes compliant with TDTM.
TDTM allows admins to change the way triggers fire with clicks, not code, including live in a production org (which wouldn’t be allowed for code deployment itself). That’s the point of it – to reintroduce a little flexibility in trigger management that isn’t baked into the Salesforce trigger-code platform.
However, it also means that admins have live-in-production control over how Apex classes fire in response to data being saved in the org, and that they might click buttons that developers of these Apex classes never intended to be clicked.
Whenever developers release a new suite of TDTM-ready trigger handlers into an org, they should include robust documentation and training for all current and future Salesforce administrators who will have to wade through the “Trigger Handlers” object in Salesforce and try to guess what its records are responsible for and whether they’re configured correctly.
Async TDTM kills context
When an admin or dev checks the “Asynchonous After Events” box in NPSP or the “Asynchronous” box in EDA on a record in the TDTM
Trigger Handler object and saves it, the corresponding Apex code loses its ability to compare internal
Trigger.old Apex values.
Trigger.old are what Apex programmers use to determine whether specific field values actually changed during the data-editing event that fired off the trigger handler.
In my opinion, it’s often a good idea for Apex trigger handler developers to write code that inspects
A large amount of computationally expensive business logic (like updating related records) might only need to happen if a record-save involved a “relevant” field’s value changing.
In fact, it seems that NPSP and EDA developers themselves make heavy use of
Trigger.old, because the TDTM admin documents for both packages warn:
“Asynchronous After Events (7)—Allows this Apex class to run asynchronously. Requires that your Apex class supports asynchronous processing. NPSP classes are not designed to run asynchronously. Test thoroughly if you enable this option.”
“Asynchronous (10)—Allows this Apex class to run asynchronously. Requires that your Apex class supports asynchronous processing. EDA classes are not designed to run asynchronously. Test thoroughly if you enable this option.”
Workaround: sync TDTM with async code
Uh-oh! Asynchronicity is important, right?
At Dreamforce, people always seem to say that if you’re struggling to get records to save because of “CPU time” or other Salesforce governor limits, you need to have your developers make as many of your triggers “asynchronous” as possible, right?
The good news is, even if admins can’t save the day on limits issues by flipping the “Asynchronous” button in TDTM, that doesn’t mean that “asynchronous triggers” can’t save the day.
It just means that developers have to program asynchronicity into the Apex trigger handler itself’s codebase.
Developers can write trigger handler Apex code to:
- Do the comparisons that they need to do of
Trigger.oldwithin an Apex “trigger handler” class summoned by TDTM synchronously.
- Make a list of all Salesforce object IDs that “are actually relevant & need to have work done on them.”
- Asynchronously invoke a different Apex class against only the pre-screened set of “IDs that need work” (rather than the larger set of “IDs that just went through record-saves” processed in step 1).
Developers, see step 3? When you program that “different Apex class” to “do the work,” there are major dev-admin cooperation advantages to architecting it as a “service layer” à la Andy Fawcett.
“Service layer” code should accept a
Set<Id> parameter indicating “IDs that need work” and then independently SOQL-query the database for any record details needed to do the work. Don’t let your service layer rely on cheats like inspecting
Trigger.new for that data, even though, yes, you’re possibly “wasting” a SOQL query.
When you architect “service classes” like this, you can easily cooperate with admins to back-fill records after introducing a new trigger handler into an org, as if the trigger handler had been active all along.
All you have to do is manually pass some IDs into Anonymous Apex that invokes your “service class.”
(Whereas under normal execution, it’s a trigger handler that decides which IDs are worth passing to your “service class.”)
Admin caveat: TDTM async breaks the workaround
Note that when developers use the “workaround” pattern above to write Apex code for trigger handlers, the codebase will get confused if it’s run in asynchronous mode.
This is because the Apex that Salesforce wrote … the Apex that makes TDTM work … lies to the Apex code that your developers are writing.
When the Asynchronous box is checked, TDTM passes fake
Trigger.old values to your custom code. Custom trigger-handler code can choke on those values.
Often, you might see that trigger handlers you thought were tested & working suddenly break. Often with a
System.NullPointerException: Attempt to de-reference a null object error that isn’t traceable to a specific line of code because it happens buried in a mysterious
(System Code) context.
When a nonprofit decides to use TDTM to control their in-house trigger handlers, it’s important to:
- Develop a system for documenting what the TDTM record for a given trigger handler should vs. shouldn’t look like.
- Make sure all developers are trained by admins in what makes for a “thorough” piece of documentation and are trained never to approve a TDTM-oriented “trigger handler” being set to “Active” in production until they’ve written up thorough documentation.
- Make sure admins are trained to read it before editing the contents of a TDTM record.
The “vs. shouldn’t” bit is important.
Developers should think ahead about:
- “If this trigger handler were deactivated, but some other trigger handler, process flow, or workflow weren’t, what might go ‘wrong’ according to business-logic expectations?”
- “If this trigger handler were set to asynchronous / unset from asynchronous, what might go ‘wrong’ with its execution?”
Admins and developers together should participate in a manual process of “playing with all of the fields on a Trigger Handler record to see what happens” part of the quality assurance testing processes in various scratch orgs and sandboxes (and documenting the results in a place that people are trained to read the docs).
Do this at the same time as other “actual dummy-checking the trigger with live data edits” QA work happens in sandboxes and scratch orgs.
Below is a bonus tip for EDA developers about testing and documentation when you’re in the process of writing unit tests.
Whereas normally maybe your unit tests might mock up TDTM records like this, using the 4-parameter
List<hed.TDTM_Global_API.TdtmToken> tokens = hed.TDTM_Global_API.getTdtmConfig();
tokens.add(new hed.TDTM_Global_API.TdtmToken('The_Apex_Handler_Class', 'The_Custom_Object__c', 'AfterInsert;AfterUpdate', 2.00));
Try using the longest available
TdtmToken constructor instead:
List<hed.TDTM_Global_API.TdtmToken> tokens = hed.TDTM_Global_API.getTdtmConfig();
tokens.add(new hed.TDTM_Global_API.TdtmToken('The_Apex_Handler_Class', 'The_Custom_Object__c', 'AfterInsert;AfterUpdate', 2.00, TRUE, FALSE, NULL, NULL, FALSE, NULL, NULL));
For example, the Active and Asynchronous checkboxes are represented by constructor parameters 5 & 6.
Once you have your full codebase written, unit tests fully written & passing, etc … play with flipping around the parameter values in
hed.TDTM_Global_API.TdtmToken(...) and run your tests again.
Wade through the tests runs’ stack trace logs, line-by-line code coverage, and
You should be able to use this information to help you document, in plain English, what an admin might expect to see happen if they were to change various values on an actual record on the Trigger Handler database object.
Once you’ve finished playing with
TdtmToken(...) parameters and documenting the results, you can put your unit test
TdtmToken(...) invocations back to “normal” so that your tests pass, announce your code is “ready for manual testing” as usual, and move forward in the QA process like you normally would.