Apex Trigger Framework and Custom Metadata Types: Zero-Downtime Logic Switching
0 reputation · 22 May 2023, 14:26 UTC
0 reputation · 22 May 2023, 14:26 UTC
Updating application logic in an Apex trigger without downtime using a Trigger Framework pattern combined with Custom Metadata Type feature flags introduces an unresolved integration boundary between synchronous trigger execution and asynchronous processing.
The platform does not guarantee execution order when multiple triggers exist on the same object, which complicates deterministic behavior during a logic switch. Additionally, asynchronous Apex operations such as Future, Queueable, or Batchable jobs may read the feature flag at a different time than the initiating transaction, creating a window where old and new logic operate on overlapping data. Governor limits on CPU time and heap size further constrain how transition states can be validated during bulk operations.
What is the recommended pattern for ensuring that asynchronous Apex jobs spawned during a feature-flag transition consistently apply the intended logic version? How does Salesforce resolve trigger execution order for multiple triggers on a single object when the underlying logic is controlled by Custom Metadata? What state-management practices prevent data inconsistency when toggling Apex logic via metadata during active bulk operations?
35275 reputation · 22 May 2023, 22:38 UTC
Use one Apex trigger per object that delegates to a handler. The handler reads a versioned Custom Metadata feature flag once per transaction and selects the logic path. Any asynchronous job—Future, Queueable, or Batchable—should receive that version from the enqueuing transaction and use it for the entire job, rather than re-reading the live flag mid-execution. Salesforce orders triggers on the same object by creation or deployment sequence, not by metadata values, so a flag switch does not reorder triggers. The main inconsistency risk is an async job that starts before the switch but reads the flag after it, or vice versa.
Salesforce executes multiple triggers on an object in a deterministic order based on when they were created. Deployments through the Metadata API follow the order in the deployment manifest. Custom Metadata values do not influence this sequence. That means a feature-flag toggle cannot change which trigger runs first; it only changes what each trigger does. If you have multiple triggers today, consolidate them into a single trigger before relying on metadata for logic switching, because the platform gives you no ordering guarantee tied to configuration.
The likely explanation for overlapping old and new logic is a timing gap: the synchronous trigger enqueues a job, the transaction commits, the metadata is updated, and the job then reads the new flag. To close that gap, add an integer Version__c to the metadata record. The trigger handler reads the flag, captures the version, and passes it to the async constructor. The job stores that value in a private final field and never re-queries the metadata during execution.
public class LogicJob implements Queueable {
private final Integer pinnedVersion;
public LogicJob(Integer v) { this.pinnedVersion = v; }
public void execute(QueueableContext ctx) {
// Branch on pinnedVersion only.
// Do not call FeatureFlag__mdt.getInstance() here.
}
}
For Batchable jobs, snapshot the version in start() and pass it to execute() through the scope or a stateful field, keeping the payload small to respect heap limits. If you cannot change the job signature, read the flag exactly once on the first line of execute() and cache it locally, but understand that this still leaves a narrow window if the switch happens between enqueue and execution.
getInstance(). Do not query or re-read it inside record loops.InTransition__c to block new async enqueues while you flip the version, preventing a flood of jobs from straddling the change.Deploy a metadata record with NewLogic__c and Version__c. Create one Account trigger that calls a handler, writes different fields based on the flag, and enqueues a Queueable that also writes based on the pinned version. Insert records, update the metadata in Setup, and inspect debug logs: jobs enqueued before the update should finish with the old version, and jobs enqueued after should use the new one.
This assumes your Apex API level supports Custom Metadata getInstance() and Queueable constructor arguments. It also assumes you can modify the async job classes; if they are managed-package code you cannot change, the recommendation shifts from version pinning to suspending enqueues during the switch. One diagnostic would change the advice: do your async jobs currently receive the version from the enqueuing transaction, or do they read the live flag inside execute()? If they read live mid-job, snapshotting at start is mandatory rather than optional.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.