<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Selavina B</title>
    <description>The latest articles on DEV Community by Selavina B (@selavina_b_de3b87f13c96a6).</description>
    <link>https://dev.to/selavina_b_de3b87f13c96a6</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3652975%2Fa92ec79a-c9fd-4ad5-ba30-3f049b54272e.png</url>
      <title>DEV Community: Selavina B</title>
      <link>https://dev.to/selavina_b_de3b87f13c96a6</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/selavina_b_de3b87f13c96a6"/>
    <language>en</language>
    <item>
      <title>Difficulty Debugging Black-Box Model Decisions (Salesforce Einstein / AI)</title>
      <dc:creator>Selavina B</dc:creator>
      <pubDate>Tue, 13 Jan 2026 12:09:42 +0000</pubDate>
      <link>https://dev.to/selavina_b_de3b87f13c96a6/difficulty-debugging-black-box-model-decisions-salesforce-einstein-ai-1if5</link>
      <guid>https://dev.to/selavina_b_de3b87f13c96a6/difficulty-debugging-black-box-model-decisions-salesforce-einstein-ai-1if5</guid>
      <description>&lt;p&gt;&lt;strong&gt;Why This Problem Happens (Root Causes)&lt;/strong&gt;&lt;br&gt;
You face this issue because:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Einstein Prediction Builder hides internal model logic&lt;/li&gt;
&lt;li&gt;Limited access to:

&lt;ul&gt;
&lt;li&gt;Feature weights&lt;/li&gt;
&lt;li&gt;SHAP / LIME values&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Stakeholders ask “Why did this prediction happen?”&lt;/li&gt;
&lt;li&gt;Wrong predictions are hard to debug&lt;/li&gt;
&lt;li&gt;Trust in AI decreases&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Identify Which Einstein Tool You Are Using&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Different tools = different explainability depth.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Einstein Tool&lt;/strong&gt;           &lt;strong&gt;Explainability Level&lt;/strong&gt;&lt;br&gt;
Prediction Builder     Low (Top factors only)&lt;br&gt;
Einstein Discovery    Medium–High (Feature contributions)&lt;br&gt;
Einstein GPT / External ML  Depends on implementation&lt;br&gt;
Custom ML via API            Full control&lt;/p&gt;

&lt;p&gt;If explainability is critical → Einstein Discovery or Custom ML&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2: Use Einstein Discovery (Built-In Explainability)&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;Why Einstein Discovery?&lt;/strong&gt;&lt;br&gt;
It is the only Einstein product with true model explanations.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What You Get:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Top predictors&lt;/li&gt;
&lt;li&gt;Prediction contributions&lt;/li&gt;
&lt;li&gt;Outcome explanations per record&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Enable Explanations (UI Steps)&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Go to Einstein Discovery&lt;/li&gt;
&lt;li&gt;Open your model&lt;/li&gt;
&lt;li&gt;Enable:
Prediction Explanations
Top Factors&lt;/li&gt;
&lt;li&gt;Deploy to Salesforce Object&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Example: Viewing Explanation on a Record&lt;/strong&gt;&lt;br&gt;
On an Opportunity record:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Prediction: Close Probability = 82%

Top Contributing Factors:
+ Amount &amp;gt; $50,000 (+18%)
+ Industry = Finance (+12%)
- Stage Duration &amp;gt; 60 days (-9%)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This builds stakeholder trust immediately&lt;/p&gt;

&lt;p&gt;*&lt;em&gt;Step 3: Store Einstein Explanations for Debugging (Best Practice)&lt;br&gt;
*&lt;/em&gt;&lt;br&gt;
Einstein explanations disappear unless you persist them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Create Custom Fields&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;AI_Prediction__c&lt;br&gt;
AI_Top_Factors__c (Long Text)&lt;br&gt;
AI_Confidence_Score__c&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Apex Example: Capture Einstein Prediction&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public class EinsteinPredictionHandler {

    public static void savePrediction(
        Id recordId,
        Decimal score,
        String explanation
    ) {
        Opportunity opp = new Opportunity(
            Id = recordId,
            AI_Prediction__c = score,
            AI_Top_Factors__c = explanation
        );
        update opp;
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;✔ Enables:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Auditing&lt;/li&gt;
&lt;li&gt;Historical debugging&lt;/li&gt;
&lt;li&gt;AI drift detection&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Step 4: Detect Wrong Predictions Systematically&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;Add Feedback Loop (Critical Step)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Create a field:&lt;br&gt;
&lt;code&gt;AI_Feedback__c (Correct / Incorrect)&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Apex Trigger to Log Wrong Predictions&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;trigger AIFeedbackTrigger on Opportunity (after update) {
    for (Opportunity opp : Trigger.new) {
        Opportunity oldOpp = Trigger.oldMap.get(opp.Id);

        if (opp.AI_Feedback__c == 'Incorrect'
            &amp;amp;&amp;amp; oldOpp.AI_Feedback__c != 'Incorrect') {

            System.debug(
                'Wrong AI prediction for record: ' + opp.Id
            );
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now you have real data to retrain models&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 5: Use Feature Contribution Analysis (Manual Debugging)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When predictions look wrong, ask:&lt;/p&gt;

&lt;p&gt;Question    Check&lt;br&gt;
Wrong input data?   Field completeness&lt;br&gt;
Bias?   Industry / Region&lt;br&gt;
Stale data? Data freshness&lt;br&gt;
Leakage?    Outcome-related fields&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example: Detect Data Leakage&lt;/strong&gt;&lt;br&gt;
Bad Feature:&lt;br&gt;
&lt;code&gt;Closed_Date__c used to predict Close_Won&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;✔ Remove it from training dataset.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 6: Advanced Explainability Using SHAP (Custom AI)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If Einstein explanations are not enough, use external ML with SHAP.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Architecture&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;Salesforce → REST API → Python ML → SHAP → Back to Salesforce&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Python Example: SHAP Explainability&lt;/strong&gt;&lt;br&gt;
import shap&lt;br&gt;
import xgboost as xgb&lt;/p&gt;

&lt;p&gt;model = xgb.XGBClassifier()&lt;br&gt;
model.load_model("model.json")&lt;/p&gt;

&lt;p&gt;explainer = shap.TreeExplainer(model)&lt;br&gt;
shap_values = explainer.shap_values(X_test)&lt;/p&gt;

&lt;p&gt;shap.summary_plot(shap_values, X_test)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Feature importance per prediction&lt;/li&gt;
&lt;li&gt;Clear reason why model predicted something&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Step 7: Push SHAP Results Back to Salesforce&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;Salesforce REST API (Python)&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import requests

payload = {
    "AI_Explanation__c": "High Amount (+0.21), Industry Finance (+0.15)"
}

requests.patch(
    "https://yourInstance.salesforce.com/services/data/v59.0/sobjects/Opportunity/006XXXX",
    headers=headers,
    json=payload
)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;✔ Salesforce becomes AI-transparent&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 8: Add Guardrails for AI Trust&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;Best Practices Checklist&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;✔ Never deploy AI without explanations&lt;br&gt;
✔ Log predictions + explanations&lt;br&gt;
✔ Allow user feedback&lt;br&gt;
✔ Monitor prediction drift&lt;br&gt;
✔ Retrain quarterly&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 9: Communicate AI Decisions to Stakeholders&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;Human-Readable Explanation Format&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Bad:&lt;br&gt;
&lt;code&gt;Model Score: 0.82&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Good:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;This opportunity has a high chance to close because:
• Deal size is above average
• Customer is in a high-conversion industry
• Sales cycle duration is within healthy range
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 10: When NOT to Use Einstein&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Avoid Einstein when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Legal compliance requires explainability&lt;/li&gt;
&lt;li&gt;Decisions affect credit, pricing, or risk&lt;/li&gt;
&lt;li&gt;Stakeholders demand transparency
Use custom ML + SHAP instead&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;🔍 Faster debugging&lt;br&gt;
🧠 Higher trust in AI&lt;br&gt;
📊 Better retraining signals&lt;br&gt;
⚖ Compliance-ready AI&lt;br&gt;
 &lt;a href="https://sdlccorp.com/certified-salesforce-development-company/" rel="noopener noreferrer"&gt;salesforce development company&lt;/a&gt; &lt;/p&gt;

</description>
      <category>ai</category>
      <category>machinelearning</category>
      <category>tooling</category>
    </item>
    <item>
      <title>Trigger Logic Causing Recursive Updates or Data Duplication</title>
      <dc:creator>Selavina B</dc:creator>
      <pubDate>Thu, 08 Jan 2026 12:40:42 +0000</pubDate>
      <link>https://dev.to/selavina_b_de3b87f13c96a6/trigger-logic-causing-recursive-updates-or-data-duplication-8aa</link>
      <guid>https://dev.to/selavina_b_de3b87f13c96a6/trigger-logic-causing-recursive-updates-or-data-duplication-8aa</guid>
      <description>&lt;p&gt;&lt;strong&gt;Problem Statement:&lt;/strong&gt; Poorly designed Apex triggers run multiple times per transaction, causing recursive updates, duplicate records, or unintended field changes due to missing recursion guards, mixed responsibilities, or lack of a trigger framework.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Salesforce Trigger Logic Causing Recursive Updates or Data Duplication&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;Step-by-Step Fix (with Code)&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;Why This Happens in Salesforce&lt;/strong&gt;&lt;br&gt;
Salesforce triggers can run multiple times in a single transaction:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;before insert&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;after insert&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;before update&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;after update&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Recursion and duplication occur when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A trigger updates the same object it fires on&lt;/li&gt;
&lt;li&gt;Multiple triggers handle the same logic&lt;/li&gt;
&lt;li&gt;No recursion guard is used&lt;/li&gt;
&lt;li&gt;Logic is scattered across trigger contexts&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Common Symptoms&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Duplicate child records created&lt;/li&gt;
&lt;li&gt;Fields updated repeatedly&lt;/li&gt;
&lt;li&gt;CPU time limit exceeded&lt;/li&gt;
&lt;li&gt;“Too many DML statements” errors&lt;/li&gt;
&lt;li&gt;Records updated even when values didn’t change&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Step 1 Identify the BAD trigger pattern&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;WRONG: Updating records inside after update without guard&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;trigger AccountTrigger on Account (after update) {
    for (Account acc : Trigger.new) {
        acc.Description = 'Updated';
    }
    update Trigger.new; //  Recursive trigger
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;This re-fires the trigger endlessly.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2 Move logic to a Trigger Handler Framework&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;Trigger (Single Responsibility)&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;trigger AccountTrigger on Account (
    before insert, before update,
    after insert, after update
) {
    AccountTriggerHandler.run(
        Trigger.operationType,
        Trigger.new,
        Trigger.oldMap
    );
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;One trigger per object&lt;/li&gt;
&lt;li&gt;No logic inside trigger&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Step 3 Add a Recursion Guard (Critical)&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;Static Boolean Guard&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public class TriggerControl {
    public static Boolean isRunning = false;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 4 Implement Guarded Logic in Handler&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public class AccountTriggerHandler {

    public static void run(
        System.TriggerOperation operation,
        List&amp;lt;Account&amp;gt; newList,
        Map&amp;lt;Id, Account&amp;gt; oldMap
    ) {
        if (TriggerControl.isRunning) return;
        TriggerControl.isRunning = true;

        if (operation == System.TriggerOperation.BEFORE_UPDATE) {
            beforeUpdate(newList, oldMap);
        }

        if (operation == System.TriggerOperation.AFTER_INSERT) {
            afterInsert(newList);
        }

        TriggerControl.isRunning = false;
    }
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Prevents recursive execution&lt;/li&gt;
&lt;li&gt;Centralized control&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Step 5 Only Update Records When Data Actually Changes&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;BAD&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;acc.Status__c = 'Active';
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Runs every time → recursion.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GOOD&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;if (acc.Status__c != 'Active') {
    acc.Status__c = 'Active';
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Prevents unnecessary updates&lt;/li&gt;
&lt;li&gt;Reduces trigger re-fires&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Step 6 Never Insert Child Records Without Duplication Checks&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;BAD (duplicates)&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;insert new Contact(
    LastName = 'Auto',
    AccountId = acc.Id
);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;GOOD (check existing records)&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Map&amp;lt;Id, Integer&amp;gt; contactCountMap = new Map&amp;lt;Id, Integer&amp;gt;();

for (AggregateResult ar : [
    SELECT AccountId accId, COUNT(Id) cnt
    FROM Contact
    WHERE AccountId IN :accountIds
    GROUP BY AccountId
]) {
    contactCountMap.put((Id) ar.get('accId'), (Integer) ar.get('cnt'));
}

List&amp;lt;Contact&amp;gt; contactsToInsert = new List&amp;lt;Contact&amp;gt;();
for (Account acc : newList) {
    if (!contactCountMap.containsKey(acc.Id)) {
        contactsToInsert.add(
            new Contact(LastName = 'Auto', AccountId = acc.Id)
        );
    }
}
insert contactsToInsert;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;No duplicates&lt;/li&gt;
&lt;li&gt;Bulk-safe&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Step 7 Use before Triggers Instead of after When Possible&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;BAD&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;after update → update same record
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;GOOD&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;before update → modify Trigger.new

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;if (acc.Score__c == null) {
    acc.Score__c = 0;
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;No DML&lt;/li&gt;
&lt;li&gt;No recursion&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Step 8 Split Logic by Responsibility&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;BAD&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Validation&lt;/li&gt;
&lt;li&gt;Field updates&lt;/li&gt;
&lt;li&gt;Record creation&lt;/li&gt;
&lt;li&gt;Callouts
All in one trigger.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;GOOD Architecture&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;AccountTriggerHandler&lt;/li&gt;
&lt;li&gt;AccountService&lt;/li&gt;
&lt;li&gt;AccountValidator&lt;/li&gt;
&lt;li&gt;AccountAsyncProcessor&lt;/li&gt;
&lt;li&gt;Easier testing&lt;/li&gt;
&lt;li&gt;No duplicate execution&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Step 9 Prevent Cross-Object Trigger Loops&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;COMMON LOOP&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Account trigger updates Contact&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Contact trigger updates Account&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;FIX Shared Static Guard&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public class GlobalTriggerState {
    public static Set&amp;lt;String&amp;gt; executed = new Set&amp;lt;String&amp;gt;();
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;if (GlobalTriggerState.executed.contains('Account')) return;
GlobalTriggerState.executed.add('Account');
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;ul&gt;
&lt;li&gt;Prevents infinite cross-object loops&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Step 10 Final Safe Trigger Template (Production-Ready)&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;trigger AccountTrigger on Account (before update, after insert) {
    if (TriggerControl.isRunning) return;
    TriggerControl.isRunning = true;

    if (Trigger.isBefore &amp;amp;&amp;amp; Trigger.isUpdate) {
        for (Account acc : Trigger.new) {
            if (acc.Status__c != 'Active') {
                acc.Status__c = 'Active';
            }
        }
    }

    if (Trigger.isAfter &amp;amp;&amp;amp; Trigger.isInsert) {
        AccountService.createDefaultContacts(Trigger.new);
    }

    TriggerControl.isRunning = false;
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Conclusion&lt;/strong&gt;&lt;br&gt;
In &lt;a href="https://sdlccorp.com/certified-salesforce-development-company/" rel="noopener noreferrer"&gt;Salesforce Development&lt;/a&gt;, recursive triggers and data duplication are not platform bugs they are design issues. The fix is disciplined trigger architecture: one trigger per object, handler classes, recursion guards, before-trigger updates, and strict duplication checks. When triggers are treated as event routers instead of logic containers, recursion stops, data stays clean, and your org becomes stable and scalable.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>backend</category>
      <category>codequality</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Performance Degradation Due to Inefficient Apex / ORM Usage (salesforce)</title>
      <dc:creator>Selavina B</dc:creator>
      <pubDate>Wed, 07 Jan 2026 09:31:54 +0000</pubDate>
      <link>https://dev.to/selavina_b_de3b87f13c96a6/performance-degradation-due-to-inefficient-apex-orm-usage-salesforce-2h6e</link>
      <guid>https://dev.to/selavina_b_de3b87f13c96a6/performance-degradation-due-to-inefficient-apex-orm-usage-salesforce-2h6e</guid>
      <description>&lt;p&gt;&lt;strong&gt;Problem Statement:&lt;/strong&gt; Slow page loads, long batch jobs, or timeouts occur when Apex code is not bulk-safe, triggers perform row-by-row operations, or inefficient queries are used, leading to governor limit exceptions and poor org performance.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1 Identify Inefficient Apex Patterns&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;Common Bad Practice&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;trigger AccountTrigger on Account (after insert, after update) {
    for (Account acc : Trigger.new) {
        // N+1 query inside loop
        List&amp;lt;Contact&amp;gt; contacts = [SELECT Id, Email FROM Contact WHERE AccountId = :acc.Id];
        for (Contact c : contacts) {
            c.Email = acc.Name + '.contact@example.com';
            update c; // DML inside loop
        }
    }
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Problems:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;SOQL in a loop → N+1 problem&lt;/li&gt;
&lt;li&gt;DML in a loop → governor limit errors&lt;/li&gt;
&lt;li&gt;Not bulk-safe → fails on large record batches&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Step 2 Make Apex Code Bulk-Safe&lt;/strong&gt;&lt;br&gt;
Rule: Always process collections (lists, maps, sets) and avoid per-record queries or DML inside loops.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bulk-Safe Version&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;trigger AccountTrigger on Account (after insert, after update) {
    // Step 1: Collect all Account IDs
    Set&amp;lt;Id&amp;gt; accountIds = new Set&amp;lt;Id&amp;gt;();
    for (Account acc : Trigger.new) {
        accountIds.add(acc.Id);
    }

    // Step 2: Query all related contacts at once
    List&amp;lt;Contact&amp;gt; contactsToUpdate = [
        SELECT Id, Email, AccountId 
        FROM Contact 
        WHERE AccountId IN :accountIds
    ];

    // Step 3: Prepare contacts for update
    for (Contact c : contactsToUpdate) {
        Account parent = Trigger.newMap.get(c.AccountId);
        c.Email = parent.Name + '.contact@example.com';
    }

    // Step 4: Bulk DML
    if (!contactsToUpdate.isEmpty()) {
        update contactsToUpdate;
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Benefits:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Single SOQL query → avoids N+1 problem&lt;/li&gt;
&lt;li&gt;Single DML statement → governor-safe&lt;/li&gt;
&lt;li&gt;Works with hundreds or thousands of records&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Step 3 Optimize SOQL Queries&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use relationship queries when possible
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;List&amp;lt;Account&amp;gt; accounts = [
    SELECT Id, Name, (SELECT Id, Email FROM Contacts) 
    FROM Account
    WHERE Industry = 'Technology'
];
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Avoid unnecessary fields: &lt;code&gt;SELECT Id, Name&lt;/code&gt; instead of &lt;code&gt;SELECT *&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Filter records using &lt;code&gt;WHERE&lt;/code&gt; and &lt;code&gt;LIMIT&lt;/code&gt; to reduce data size&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Step 4 Use Maps for Fast Lookups&lt;/strong&gt;&lt;br&gt;
Instead of iterating nested loops, use maps to improve performance:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Map&amp;lt;Id, Account&amp;gt; accountMap = new Map&amp;lt;Id, Account&amp;gt;(Trigger.new);

for (Contact c : contactsToUpdate) {
    Account parent = accountMap.get(c.AccountId);
    c.Email = parent.Name + '.contact@example.com';
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Lookup in O(1) instead of O(n²)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 5 Move Logic to Handler Classes&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Trigger:

trigger AccountTrigger on Account (after insert, after update) {
    AccountTriggerHandler.handleAfter(Trigger.new, Trigger.oldMap);
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Handler Class:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public class AccountTriggerHandler {
    public static void handleAfter(List&amp;lt;Account&amp;gt; newList, Map&amp;lt;Id, Account&amp;gt; oldMap) {
        Set&amp;lt;Id&amp;gt; accIds = new Set&amp;lt;Id&amp;gt;();
        for (Account acc : newList) accIds.add(acc.Id);

        List&amp;lt;Contact&amp;gt; contacts = [SELECT Id, AccountId FROM Contact WHERE AccountId IN :accIds];
        Map&amp;lt;Id, Account&amp;gt; accountMap = new Map&amp;lt;Id, Account&amp;gt;(newList);

        for (Contact c : contacts) {
            c.Email = accountMap.get(c.AccountId).Name + '.contact@example.com';
        }

        if (!contacts.isEmpty()) update contacts;
    }
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Benefits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cleaner code&lt;/li&gt;
&lt;li&gt;Easier debugging&lt;/li&gt;
&lt;li&gt;Easy to reuse in batch jobs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Step 6 — Use Batch Apex for Large Data Volumes&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For large record sets, Batch Apex is essential:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;global class UpdateContactsBatch implements Database.Batchable&amp;lt;SObject&amp;gt; {
    global Database.QueryLocator start(Database.BatchableContext BC) {
        return Database.getQueryLocator([SELECT Id, AccountId FROM Contact]);
    }

    global void execute(Database.BatchableContext BC, List&amp;lt;Contact&amp;gt; scope) {
        Set&amp;lt;Id&amp;gt; accountIds = new Set&amp;lt;Id&amp;gt;();
        for (Contact c : scope) accountIds.add(c.AccountId);

        Map&amp;lt;Id, Account&amp;gt; accMap = new Map&amp;lt;Id, Account&amp;gt;([
            SELECT Id, Name FROM Account WHERE Id IN :accountIds
        ]);

        for (Contact c : scope) {
            c.Email = accMap.get(c.AccountId).Name + '.contact@example.com';
        }

        update scope;
    }

    global void finish(Database.BatchableContext BC) {}
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Processes 50k+ records safely&lt;/li&gt;
&lt;li&gt;Avoids governor limits&lt;/li&gt;
&lt;li&gt;Bulk-safe architecture&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Step 7 Use @future or Queueable for Non-Critical Operations&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For operations not required immediately (like sending emails or updating related objects):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@future

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;public static void updateContactEmails(Set accountIds) {&lt;br&gt;
    List contacts = [SELECT Id, AccountId FROM Contact WHERE AccountId IN :accountIds];&lt;br&gt;
    for (Contact c : contacts) {&lt;br&gt;
        c.Email = [SELECT Name FROM Account WHERE Id = :c.AccountId].Name + '&lt;a href="mailto:.contact@example.com"&gt;.contact@example.com&lt;/a&gt;';&lt;br&gt;
    }&lt;br&gt;
    update contacts;&lt;br&gt;
}&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Moves heavy work out of synchronous transaction&lt;/li&gt;
&lt;li&gt;Reduces UI latency&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Step 8 Monitor and Test&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Enable Debug Logs → check SOQL and DML statements&lt;/li&gt;
&lt;li&gt;Test with bulk records (200+)&lt;/li&gt;
&lt;li&gt;Use Query Plan Tool for expensive queries&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Conclusion&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Performance degradation in Salesforce is almost always caused by inefficient Apex code or improper ORM usage. By making triggers bulk-safe, using batch operations, minimizing SOQL/DML inside loops, and separating logic into handler classes or batch jobs, you can eliminate slow page loads, prevent governor limit errors, and scale your org for large datasets efficiently.&lt;/p&gt;

</description>
      <category>backend</category>
      <category>codequality</category>
      <category>database</category>
      <category>performance</category>
    </item>
    <item>
      <title>Salesforce Apex Code Failing Due to Lack of Bulkification</title>
      <dc:creator>Selavina B</dc:creator>
      <pubDate>Tue, 30 Dec 2025 12:23:58 +0000</pubDate>
      <link>https://dev.to/selavina_b_de3b87f13c96a6/salesforce-apex-code-failing-due-to-lack-of-bulkification-p32</link>
      <guid>https://dev.to/selavina_b_de3b87f13c96a6/salesforce-apex-code-failing-due-to-lack-of-bulkification-p32</guid>
      <description>&lt;p&gt;&lt;strong&gt;Salesforce Apex Code Failing Due to Lack of Bulkification (And How to Fix It)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you are developing on &lt;strong&gt;Salesforce&lt;/strong&gt;, sooner or later you will face this error:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Too many SOQL queries: 101&lt;/li&gt;
&lt;li&gt;Too many DML statements&lt;/li&gt;
&lt;li&gt;CPU time limit exceeded&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In most cases, the root cause is simple but dangerous:&lt;br&gt;
&lt;strong&gt;your Apex code is not bulkified&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This issue usually does not appear during development or testing with single records. It shows up suddenly in production during data imports, integrations, or batch jobs and breaks business processes.&lt;/p&gt;

&lt;p&gt;This post explains &lt;strong&gt;what bulkification really means&lt;/strong&gt;, &lt;strong&gt;why Apex fails without it&lt;/strong&gt;, and &lt;strong&gt;how to fix it properly with code&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Real Problem&lt;/strong&gt;&lt;br&gt;
Salesforce does not execute Apex code &lt;strong&gt;one record at a time&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Instead:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Triggers can receive &lt;strong&gt;up to 200 records&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Batch Apex can process &lt;strong&gt;thousands&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Data Loader and integrations insert records in bulk&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your Apex logic assumes &lt;strong&gt;single-record execution&lt;/strong&gt;, Salesforce governor limits will be exceeded.&lt;/p&gt;

&lt;p&gt;That’s when things fail.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Understand How Salesforce Executes Apex&lt;/strong&gt;&lt;br&gt;
Consider this trigger:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;trigger AccountTrigger on Account (before insert) {
    for (Account acc : Trigger.new) {
        // Your logic here
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This trigger might:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Run for &lt;strong&gt;1 record&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Or &lt;strong&gt;200 records at once&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Salesforce does not guarantee single-record execution.&lt;br&gt;
Your code must always expect bulk input.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2: Identify Non-Bulkified Code&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Non-bulkified Apex&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;trigger ContactTrigger on Contact (after insert) {
    for (Contact c : Trigger.new) {
        Account acc = [
            SELECT Id, Name
            FROM Account
            WHERE Id = :c.AccountId
        ];

        acc.Description = 'Updated';
        update acc;
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why this fails&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One SOQL query per record&lt;/li&gt;
&lt;li&gt;One DML per record&lt;/li&gt;
&lt;li&gt;200 records = 200 queries + 200 DML&lt;/li&gt;
&lt;li&gt;Governor limits exceeded&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This code &lt;strong&gt;will break in production&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3: Bulkify SOQL Queries (The Right Way)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Correct bulkified approach&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;trigger ContactTrigger on Contact (after insert) {

    Set&amp;lt;Id&amp;gt; accountIds = new Set&amp;lt;Id&amp;gt;();

    for (Contact c : Trigger.new) {
        if (c.AccountId != null) {
            accountIds.add(c.AccountId);
        }
    }

    Map&amp;lt;Id, Account&amp;gt; accountMap = new Map&amp;lt;Id, Account&amp;gt;(
        [SELECT Id, Description FROM Account WHERE Id IN :accountIds]
    );

    for (Account acc : accountMap.values()) {
        acc.Description = 'Updated';
    }

    update accountMap.values();
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What changed&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One SOQL query&lt;/li&gt;
&lt;li&gt;One DML statement&lt;/li&gt;
&lt;li&gt;Works for 1 record or 200 records&lt;/li&gt;
&lt;li&gt;Safe for production&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Step 4: Never Put SOQL or DML Inside Loops&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is the &lt;strong&gt;golden rule of Apex&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;What NOT to do&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;for (Opportunity opp : Trigger.new) {
    insert new Task(WhatId = opp.Id);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Correct approach&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;List&amp;lt;Task&amp;gt; tasks = new List&amp;lt;Task&amp;gt;();

for (Opportunity opp : Trigger.new) {
    tasks.add(new Task(WhatId = opp.Id));
}

insert tasks;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 5: Bulkify Helper Classes and Services&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Bulkification must apply &lt;strong&gt;everywhere&lt;/strong&gt;, not just triggers.&lt;/p&gt;

&lt;p&gt;Hidden problem&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public class AccountService {
    public static void updateAccount(Id accId) {
        Account acc = [SELECT Id FROM Account WHERE Id = :accId];
        acc.Name = 'Updated';
        update acc;
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Calling this method in a loop will break limits.&lt;/p&gt;

&lt;p&gt;Bulk-safe service method&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public class AccountService {
    public static void updateAccounts(Set&amp;lt;Id&amp;gt; accountIds) {

        List&amp;lt;Account&amp;gt; accounts = [
            SELECT Id, Name FROM Account WHERE Id IN :accountIds
        ];

        for (Account acc : accounts) {
            acc.Name = 'Updated';
        }

        update accounts;
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 6: Use Trigger Handler Pattern (Highly Recommended)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Multiple triggers increase risk of non-bulkified logic.&lt;/p&gt;

&lt;p&gt;Trigger&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;trigger AccountTrigger on Account (before update) {
    AccountHandler.beforeUpdate(Trigger.new);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Handler&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public class AccountHandler {
    public static void beforeUpdate(List&amp;lt;Account&amp;gt; accounts) {
        for (Account acc : accounts) {
            // Bulk-safe logic
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This pattern:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Forces bulk handling&lt;/li&gt;
&lt;li&gt;Improves readability&lt;/li&gt;
&lt;li&gt;Simplifies testing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Step 7: Write Bulk Test Cases (Mandatory)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If your test inserts only one record, your code is &lt;strong&gt;not truly tested&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Weak test&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;insert new Account(Name = 'Test');
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Proper bulk test&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;List&amp;lt;Account&amp;gt; accounts = new List&amp;lt;Account&amp;gt;();

for (Integer i = 0; i &amp;lt; 200; i++) {
    accounts.add(new Account(Name = 'Test ' + i));
}

insert accounts;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If this test fails → your code is not bulkified.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 8: Watch These Governor Limits Closely&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Limit&lt;/th&gt;
&lt;th&gt;Max&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;SOQL queries&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DML statements&lt;/td&gt;
&lt;td&gt;150&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Records per DML&lt;/td&gt;
&lt;td&gt;10,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CPU time&lt;/td&gt;
&lt;td&gt;10 sec (sync)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Bulkification is the only way to stay within these limits.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key Takeaways&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Salesforce Apex code usually fails in production not because it is wrong—but because it is &lt;strong&gt;written for single records&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;To avoid failures:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Assume every trigger runs on 200 records&lt;/li&gt;
&lt;li&gt;Never put SOQL or DML inside loops&lt;/li&gt;
&lt;li&gt;Use collections (Set, Map, List)&lt;/li&gt;
&lt;li&gt;Bulkify service classes&lt;/li&gt;
&lt;li&gt;Test with bulk data, not single records&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once you adopt bulk-first thinking, Apex becomes &lt;strong&gt;stable, scalable, and production-safe&lt;/strong&gt;.&lt;/p&gt;

</description>
      <category>salesforce</category>
      <category>apexcode</category>
      <category>failure</category>
      <category>bulkification</category>
    </item>
    <item>
      <title>Salesforce integration failures with external systems</title>
      <dc:creator>Selavina B</dc:creator>
      <pubDate>Mon, 29 Dec 2025 12:32:07 +0000</pubDate>
      <link>https://dev.to/selavina_b_de3b87f13c96a6/salesforce-integration-failures-with-external-systems-4n5a</link>
      <guid>https://dev.to/selavina_b_de3b87f13c96a6/salesforce-integration-failures-with-external-systems-4n5a</guid>
      <description>&lt;p&gt;&lt;strong&gt;Salesforce Integration Failures: How I Debugged and Fixed API Callout Issues in Production&lt;/strong&gt;&lt;br&gt;
Salesforce integrations look simple on paper.&lt;br&gt;
Make a REST call. Send data. Get a response.&lt;/p&gt;

&lt;p&gt;In reality, Salesforce integrations fail in silent, unpredictable ways, especially in production:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Data sync stops without warning&lt;/li&gt;
&lt;li&gt;Callouts fail intermittently&lt;/li&gt;
&lt;li&gt;Authentication suddenly breaks&lt;/li&gt;
&lt;li&gt;Jobs keep retrying but never succeed
I recently faced this issue in a live Salesforce org, and the root cause was not one thing—it was a combination of bad assumptions, missing error handling, and weak observability.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This post explains why Salesforce integrations fail, and how to fix them properly, step by step.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Core Problem&lt;/strong&gt;&lt;br&gt;
Salesforce integrations with external systems fail mainly because:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Callout errors are not logged properly&lt;/li&gt;
&lt;li&gt;Authentication tokens expire silently&lt;/li&gt;
&lt;li&gt;Timeout and retry logic is missing&lt;/li&gt;
&lt;li&gt;Errors happen inside async jobs (Batch, Queueable, Future)&lt;/li&gt;
&lt;li&gt;Developers assume APIs behave consistently&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Salesforce does not automatically “protect” your integration.&lt;br&gt;
If you don’t handle failures explicitly, they go unnoticed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Confirm the Integration Entry Point&lt;/strong&gt;&lt;br&gt;
First, identify where the integration is running:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Trigger-based callout?&lt;/li&gt;
&lt;li&gt;Queueable Apex?&lt;/li&gt;
&lt;li&gt;Batch Apex?&lt;/li&gt;
&lt;li&gt;Scheduled job?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most failures happen inside asynchronous Apex, not UI-triggered flows.&lt;/p&gt;

&lt;p&gt;Example (Queueable integration):&lt;/p&gt;

&lt;p&gt;&lt;code&gt;System.enqueueJob(new SyncOrderJob(orderId));&lt;br&gt;
&lt;/code&gt;&lt;br&gt;
If this job fails, the UI will never show an error.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2: Never Call External APIs Directly From Triggers&lt;/strong&gt;&lt;br&gt;
This is a very common Salesforce mistake.&lt;br&gt;
Wrong approach&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;trigger OrderTrigger on Order__c (after insert) {
    HttpRequest req = new HttpRequest();
    // Callout directly from trigger
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This causes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Callout exceptions&lt;/li&gt;
&lt;li&gt;Governor limit issues&lt;/li&gt;
&lt;li&gt;Unpredictable failures&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Correct architecture&lt;/strong&gt;&lt;br&gt;
Trigger → Queueable → Callout&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;trigger OrderTrigger on Order__c (after insert) {
    System.enqueueJob(new SyncOrderJob(Trigger.new));
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 3: Use Named Credentials (Non-Negotiable)&lt;/strong&gt;&lt;br&gt;
Hardcoding endpoints or tokens will break in production.&lt;br&gt;
&lt;strong&gt;Bad practice&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;req.setEndpoint('https://api.partner.com/v1/orders');
req.setHeader('Authorization', 'Bearer hardcoded_token');

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Correct approach (Named Credential)&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Go to Setup → Named Credentials&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Configure:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Base URL&lt;/li&gt;
&lt;li&gt;Authentication (OAuth 2.0 / JWT / API Key)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Use it in Apex&lt;br&gt;
&lt;code&gt;req.setEndpoint('callout:Partner_API/orders');&lt;br&gt;
&lt;/code&gt;&lt;br&gt;
Benefits:&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;Automatic token refresh&lt;/li&gt;
&lt;li&gt;Environment-safe&lt;/li&gt;
&lt;li&gt;More secure&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Step 4: Always Wrap Callouts in try–catch&lt;/strong&gt;&lt;br&gt;
This is the main reason Salesforce integrations fail silently.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Common mistake&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;HttpResponse res = http.send(req);
System.debug(res.getBody());

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If it fails → job dies → no visibility.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Production-safe code&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;try {
    Http http = new Http();
    HttpResponse res = http.send(req);

    if (res.getStatusCode() != 200) {
        throw new CalloutException(
            'API Error: ' + res.getStatus() + ' - ' + res.getBody()
        );
    }

} catch (Exception e) {
    System.debug('Integration failed: ' + e.getMessage());
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 5: Log Failures (Do Not Rely on Debug Logs)&lt;/strong&gt;&lt;br&gt;
Debug logs are temporary and unreliable in production.&lt;/p&gt;

&lt;p&gt;Create a custom Integration_Log__c object.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public static void logError(String source, String message) {
    Integration_Log__c log = new Integration_Log__c(
        Source__c = source,
        Error_Message__c = message
    );
    insert log;
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use it in catch blocks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;catch (Exception e) {
    logError('Order Sync', e.getMessage());
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now failures are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Persistent&lt;/li&gt;
&lt;li&gt;Reportable&lt;/li&gt;
&lt;li&gt;Auditable&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Step 6: Handle Timeouts Explicitly&lt;/strong&gt;&lt;br&gt;
Salesforce has strict timeout limits.&lt;br&gt;
&lt;code&gt;req.setTimeout(10000); // 10 seconds&lt;br&gt;
&lt;/code&gt;&lt;br&gt;
If your external API is slow and you don’t set this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Salesforce terminates the request&lt;/li&gt;
&lt;li&gt;No clear error is shown&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Step 7: Use Queueable Apex for Retries&lt;/strong&gt;&lt;br&gt;
Salesforce does not auto-retry failed callouts.&lt;/p&gt;

&lt;p&gt;Add retry logic manually.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;if (retryCount &amp;lt; 3) {
    System.enqueueJob(new SyncOrderJob(orderId, retryCount + 1));
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This alone fixes many “random” integration failures.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 8: Check Remote Site Settings (Classic Issue)&lt;/strong&gt;&lt;br&gt;
If you are not using Named Credentials, your callout will fail silently.&lt;/p&gt;

&lt;p&gt;Go to:&lt;br&gt;
Setup → Remote Site Settings&lt;br&gt;
&lt;code&gt;https://api.partner.com&lt;br&gt;
&lt;/code&gt;&lt;br&gt;
Missing this = guaranteed failure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 9: Test Callouts Properly (Mocking Required)&lt;/strong&gt;&lt;br&gt;
Salesforce will not allow real callouts in tests.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@isTest
private class OrderSyncTest {
    static testMethod void testCallout() {
        Test.setMock(HttpCalloutMock.class, new MockResponse());
        Test.startTest();
        System.enqueueJob(new SyncOrderJob(orderId));
        Test.stopTest();
    }
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without mocks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Tests fail&lt;/li&gt;
&lt;li&gt;Deployment blocked&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Step 10: Monitor Async Jobs Regularly&lt;/p&gt;

&lt;p&gt;Go to:&lt;br&gt;
Setup → Apex Jobs&lt;/p&gt;

&lt;p&gt;Look for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Failed Queueables&lt;/li&gt;
&lt;li&gt;Aborted Batch jobs&lt;/li&gt;
&lt;li&gt;Long-running executions
Most integration failures show up here first.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Key Takeaways&lt;/strong&gt;&lt;br&gt;
Salesforce integrations don’t fail because Salesforce is weak.&lt;br&gt;
They fail because developers trust external systems too much.&lt;/p&gt;

&lt;p&gt;To make integrations production-safe:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Always use Named Credentials&lt;/li&gt;
&lt;li&gt;Never call APIs from triggers&lt;/li&gt;
&lt;li&gt;Log every failure&lt;/li&gt;
&lt;li&gt;Handle retries explicitly&lt;/li&gt;
&lt;li&gt;Treat async jobs as first-class systems&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once these practices are in place, Salesforce integrations become stable, observable, and predictable.&lt;/p&gt;

</description>
      <category>salesforce</category>
      <category>integration</category>
      <category>failures</category>
      <category>externalsystem</category>
    </item>
    <item>
      <title>Trigger Logic Causing Recursive Updates or Data Duplication (Salesforce)</title>
      <dc:creator>Selavina B</dc:creator>
      <pubDate>Tue, 23 Dec 2025 05:25:57 +0000</pubDate>
      <link>https://dev.to/selavina_b_de3b87f13c96a6/trigger-logic-causing-recursive-updates-or-data-duplicationstep-by-step-fix-with-code-4poo</link>
      <guid>https://dev.to/selavina_b_de3b87f13c96a6/trigger-logic-causing-recursive-updates-or-data-duplicationstep-by-step-fix-with-code-4poo</guid>
      <description>&lt;p&gt;&lt;strong&gt;Problem statement&lt;/strong&gt;&lt;br&gt;
Poorly designed Apex triggers run multiple times per transaction, causing recursive updates, duplicate records, or unintended field changes because of missing recursion guards, mixed responsibilities, or lack of a trigger framework.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1 Understand WHY recursion happens&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Triggers can fire multiple times in a single transaction:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;before insert&lt;/code&gt;&lt;br&gt;
&lt;code&gt;after insert&lt;/code&gt;&lt;br&gt;
&lt;code&gt;before update&lt;/code&gt;&lt;br&gt;
&lt;code&gt;after update&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Common causes of recursion&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Trigger updates the same object again&lt;/li&gt;
&lt;li&gt;Trigger updates a related object that triggers back&lt;/li&gt;
&lt;li&gt;Workflow / Flow / Process Builder updates fields → re-fires    trigger&lt;/li&gt;
&lt;li&gt;No guard to stop re-execution&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Example of bad trigger logic&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;trigger AccountTrigger on Account (after update) {
    for (Account acc : Trigger.new) {
        acc.Description = 'Updated'; // causes recursion
    }
    update Trigger.new;
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;This will loop until governor limits are hit.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2 Use ONE trigger per object (no logic inside)&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;Correct trigger structure&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Triggers should only:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Detect context&lt;/li&gt;
&lt;li&gt;Call a handler
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;trigger AccountTrigger on Account (
    before insert, before update,
    after insert, after update
) {
    AccountTriggerHandler.run(
        Trigger.new,
        Trigger.oldMap,
        Trigger.isInsert,
        Trigger.isUpdate,
        Trigger.isBefore,
        Trigger.isAfter
    );
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;No logic. No loops. No DML here.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3 Add a recursion guard (MANDATORY)&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;Static Boolean Guard Pattern (most reliable)&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public class TriggerGuard {
    private static Set&amp;lt;String&amp;gt; executed = new Set&amp;lt;String&amp;gt;();

    public static Boolean shouldRun(String key) {
        if (executed.contains(key)) {
            return false;
        }
        executed.add(key);
        return true;
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;This guard works per transaction.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 4 Implement a proper Trigger Handler&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public with sharing class AccountTriggerHandler {

    public static void run(
        List&amp;lt;Account&amp;gt; newList,
        Map&amp;lt;Id, Account&amp;gt; oldMap,
        Boolean isInsert,
        Boolean isUpdate,
        Boolean isBefore,
        Boolean isAfter
    ) {
        if (isBefore &amp;amp;&amp;amp; isInsert) {
            beforeInsert(newList);
        }
        if (isBefore &amp;amp;&amp;amp; isUpdate) {
            beforeUpdate(newList, oldMap);
        }
        if (isAfter &amp;amp;&amp;amp; isInsert) {
            afterInsert(newList);
        }
        if (isAfter &amp;amp;&amp;amp; isUpdate) {
            afterUpdate(newList, oldMap);
        }
    }

    // ------------------ BEFORE INSERT ------------------
    private static void beforeInsert(List&amp;lt;Account&amp;gt; records) {
        for (Account acc : records) {
            if (acc.Description == null) {
                acc.Description = 'Created';
            }
        }
    }

    // ------------------ BEFORE UPDATE ------------------
    private static void beforeUpdate(List&amp;lt;Account&amp;gt; records, Map&amp;lt;Id, Account&amp;gt; oldMap) {
        for (Account acc : records) {
            Account oldAcc = oldMap.get(acc.Id);
            if (acc.Name != oldAcc.Name) {
                acc.Description = 'Name Changed';
            }
        }
    }

    // ------------------ AFTER INSERT ------------------
    private static void afterInsert(List&amp;lt;Account&amp;gt; records) {
        if (!TriggerGuard.shouldRun('AccountAfterInsert')) return;

        List&amp;lt;Contact&amp;gt; contacts = new List&amp;lt;Contact&amp;gt;();
        for (Account acc : records) {
            contacts.add(new Contact(
                LastName = 'Primary',
                AccountId = acc.Id
            ));
        }
        insert contacts; // safe
    }

    // ------------------ AFTER UPDATE ------------------
    private static void afterUpdate(List&amp;lt;Account&amp;gt; records, Map&amp;lt;Id, Account&amp;gt; oldMap) {
        if (!TriggerGuard.shouldRun('AccountAfterUpdate')) return;

        List&amp;lt;Account&amp;gt; toUpdate = new List&amp;lt;Account&amp;gt;();
        for (Account acc : records) {
            Account oldAcc = oldMap.get(acc.Id);

            // Only update if field actually changed
            if (acc.AnnualRevenue != oldAcc.AnnualRevenue) {
                toUpdate.add(new Account(
                    Id = acc.Id,
                    Rating = 'Hot'
                ));
            }
        }

        if (!toUpdate.isEmpty()) {
            update toUpdate; // guarded → no recursion
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 5 Prevent duplicate records (idempotency check)&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;Bad (creates duplicates)&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;insert new Contact(AccountId = acc.Id, LastName = 'Primary');&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Good (check before insert)&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Set&amp;lt;Id&amp;gt; accountIds = new Set&amp;lt;Id&amp;gt;();
for (Account acc : records) {
    accountIds.add(acc.Id);
}

Map&amp;lt;Id, Contact&amp;gt; existing = new Map&amp;lt;Id, Contact&amp;gt;();
for (Contact c : [
    SELECT Id, AccountId
    FROM Contact
    WHERE AccountId IN :accountIds
    AND LastName = 'Primary'
]) {
    existing.put(c.AccountId, c);
}

List&amp;lt;Contact&amp;gt; toInsert = new List&amp;lt;Contact&amp;gt;();
for (Account acc : records) {
    if (!existing.containsKey(acc.Id)) {
        toInsert.add(new Contact(
            LastName = 'Primary',
            AccountId = acc.Id
        ));
    }
}

insert toInsert;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 6 Never do DML in BEFORE triggers (rule)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Context&lt;/strong&gt;                   &lt;strong&gt;Allowed&lt;/strong&gt;&lt;br&gt;
before insert/update                   DML&lt;br&gt;
after insert/update                DML&lt;br&gt;
before                                 modify &lt;code&gt;Trigger.new&lt;/code&gt;&lt;br&gt;
after                                  separate update list&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 7 Handle Flow / Process Builder recursion&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Flows often cause recursion without you realizing it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Safe pattern:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Triggers handle core data logic&lt;/p&gt;

&lt;p&gt;Flows handle UI / notifications&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Detection:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If recursion still occurs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Check Record-Triggered Flows&lt;/li&gt;
&lt;li&gt;Ensure Flow entry criteria prevents re-firing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Example Flow criteria:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Run only when:
AnnualRevenue IS CHANGED = TRUE
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 8 Add tests that prove recursion is fixed&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@IsTest
private class AccountTriggerTest {

    @IsTest
    static void testNoRecursion() {
        Account acc = new Account(Name = 'Test');
        insert acc;

        Test.startTest();
        acc.AnnualRevenue = 500000;
        update acc;
        Test.stopTest();

        Account updated = [
            SELECT Rating, Description
            FROM Account
            WHERE Id = :acc.Id
        ];

        System.assertEquals('Hot', updated.Rating);
        System.assertNotEquals(null, updated.Description);
    }
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If recursion exists, this test will fail or hit limits.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 9 Golden rules to prevent recursion forever&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One trigger per object&lt;/li&gt;
&lt;li&gt;Zero business logic in triggers&lt;/li&gt;
&lt;li&gt;Use handler classes&lt;/li&gt;
&lt;li&gt;Always compare &lt;code&gt;Trigger.new&lt;/code&gt; vs &lt;code&gt;Trigger.old&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Never update records blindly&lt;/li&gt;
&lt;li&gt;Use static guards for after triggers&lt;/li&gt;
&lt;li&gt;Bulk-safe logic only&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Conclusion&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Trigger recursion and data duplication happen when Apex triggers lack clear separation of responsibilities, field-change checks, and recursion guards. By adopting a single-trigger + handler framework, using static execution guards, performing bulk-safe DML, and ensuring updates happen only when data truly changes, you can completely eliminate infinite loops, duplicate records, and unpredictable behavior. Well structured triggers are not just safer they’re easier to test, scale, and maintain in production.&lt;/p&gt;

</description>
      <category>database</category>
      <category>backend</category>
      <category>tutorial</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Integration Failures and API Callout Issues in Salesforce</title>
      <dc:creator>Selavina B</dc:creator>
      <pubDate>Tue, 16 Dec 2025 10:39:55 +0000</pubDate>
      <link>https://dev.to/selavina_b_de3b87f13c96a6/integration-failures-and-api-callout-issues-250a</link>
      <guid>https://dev.to/selavina_b_de3b87f13c96a6/integration-failures-and-api-callout-issues-250a</guid>
      <description>&lt;p&gt;&lt;strong&gt;Problem statement:&lt;/strong&gt;&lt;br&gt;
External integrations (REST/SOAP) intermittently fail due to unhandled callout errors, timeout issues, changes in external API contracts, or missing named credentials, causing data sync problems between Salesforce and external systems.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1 Fix the #1 root cause: Authentication + Endpoint setup&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;Use Named Credentials (don’t hardcode URL/tokens)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Setup → Named Credentials&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Create External Credential + Named Credential (newer setup) OR classic Named Credential.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Ensure:&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Correct base URL (prod vs sandbox)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Auth method (OAuth 2.0 / JWT / Basic)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Proper scopes/permissions&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Add required Headers if the API needs them (e.g., API key header)&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Why: Missing/incorrect Named Credentials causes intermittent 401/403 and “it works sometimes” issues due to token refresh failures.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2 Create a single, reusable Apex callout wrapper (handles errors properly)&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;Apex: Callout service with status-code handling + timeout + safe parsing&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public with sharing class ExternalApiService {
    public class ApiException extends Exception {
        public Integer statusCode;
        public String responseBody;
        public ApiException(String msg, Integer code, String body) {
            super(msg);
            statusCode = code;
            responseBody = body;
        }
    }

    public class ApiResponse {
        @AuraEnabled public Integer statusCode;
        @AuraEnabled public String body;
        @AuraEnabled public Map&amp;lt;String, Object&amp;gt; json; // tolerant parsing
    }

    // Named Credential: "Order_API"
    // Endpoint usage: callout:Order_API/path
    public static ApiResponse sendRequest(String method, String path, String jsonBody, Integer timeoutMs) {
        HttpRequest req = new HttpRequest();
        req.setMethod(method);
        req.setEndpoint('callout:Order_API' + path);
        req.setTimeout(timeoutMs == null ? 20000 : timeoutMs); // default 20s

        req.setHeader('Content-Type', 'application/json');
        req.setHeader('Accept', 'application/json');

        if (!String.isBlank(jsonBody) &amp;amp;&amp;amp; (method == 'POST' || method == 'PUT' || method == 'PATCH')) {
            req.setBody(jsonBody);
        }

        Http http = new Http();
        HttpResponse res;
        try {
            res = http.send(req);
        } catch (System.CalloutException ce) {
            // Timeouts, DNS, handshake, etc.
            throw new ApiException('Callout failed: ' + ce.getMessage(), 0, null);
        }

        ApiResponse out = new ApiResponse();
        out.statusCode = res.getStatusCode();
        out.body = res.getBody();

        // Handle common failure codes clearly
        if (out.statusCode &amp;gt;= 400) {
            throw new ApiException(
                'External API error. Status=' + out.statusCode,
                out.statusCode,
                out.body
            );
        }

        // Tolerant JSON parsing (prevents failures if API adds fields)
        if (!String.isBlank(out.body) &amp;amp;&amp;amp; out.body.trim().startsWith('{')) {
            out.json = (Map&amp;lt;String, Object&amp;gt;) JSON.deserializeUntyped(out.body);
        }
        return out;
    }
}


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What this fixes&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Intermittent failures become visible and actionable (proper exceptions + status codes)&lt;/p&gt;

&lt;p&gt;Timeouts are handled&lt;/p&gt;

&lt;p&gt;JSON parsing is resilient to “API contract changes” (new fields won’t break your code)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3 Add retries for transient failures (timeouts, 429, 5xx)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Retries should be async (Queueable) and limited.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Queueable retry worker (with simple backoff + max attempts)&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
public with sharing class ExternalSyncJob implements Queueable, Database.AllowsCallouts {
    private Id recordId;
    private Integer attempt;

    public ExternalSyncJob(Id recordId, Integer attempt) {
        this.recordId = recordId;
        this.attempt = attempt == null ? 1 : attempt;
    }

    public void execute(QueueableContext context) {
        // Example: sync an Order__c record
        Order__c ord = [
            SELECT Id, Name, Amount__c, External_Id__c
            FROM Order__c
            WHERE Id = :recordId
            LIMIT 1
        ];

        String payload = JSON.serialize(new Map&amp;lt;String, Object&amp;gt;{
            'orderName' =&amp;gt; ord.Name,
            'amount' =&amp;gt; ord.Amount__c,
            'externalId' =&amp;gt; ord.External_Id__c
        });

        try {
            ExternalApiService.ApiResponse resp =
                ExternalApiService.sendRequest('POST', '/orders', payload, 20000);

            // Mark success
            ord.Last_Sync_Status__c = 'SUCCESS';
            ord.Last_Sync_Message__c = 'Synced OK';
            update ord;

        } catch (ExternalApiService.ApiException ex) {
            // Retry only for transient cases
            Boolean transientFail =
                ex.statusCode == 0 || ex.statusCode == 429 || (ex.statusCode &amp;gt;= 500 &amp;amp;&amp;amp; ex.statusCode &amp;lt;= 599);

            logIntegration('Order Sync', recordId, 'ERROR',
                'Attempt ' + attempt + ' failed. Status=' + ex.statusCode,
                ex.responseBody
            );

            if (transientFail &amp;amp;&amp;amp; attempt &amp;lt; 3) {
                // Re-enqueue (basic retry). For real backoff, schedule later (see note below).
                System.enqueueJob(new ExternalSyncJob(recordId, attempt + 1));
            } else {
                ord.Last_Sync_Status__c = 'FAILED';
                ord.Last_Sync_Message__c = 'Final failure: ' + ex.getMessage();
                update ord;
            }
        }
    }

    private static void logIntegration(String operation, Id relatedId, String level, String message, String body) {
        try {
            Integration_Log__c log = new Integration_Log__c(
                Operation__c = operation,
                Related_Record_Id__c = String.valueOf(relatedId),
                Level__c = level,
                Message__c = message,
                Payload__c = body
            );
            insert log;
        } catch (Exception ignore) {}
    }
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you want true backoff delays (e.g., wait 5 min), use Schedulable or Platform Events + subscriber. Queueable runs immediately.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 4 Prevent “contract change” breaks with tolerant parsing&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you currently do strict deserialization like:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;MyDto dto = (MyDto) JSON.deserialize(body, MyDto.class);&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;This can break when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;API changes data type&lt;/li&gt;
&lt;li&gt;Field becomes null&lt;/li&gt;
&lt;li&gt;Response shape changes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Safer parsing pattern&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
Map&amp;lt;String, Object&amp;gt; m = (Map&amp;lt;String, Object&amp;gt;) JSON.deserializeUntyped(body);
Object value = m.get('status'); // check exists
String status = value == null ? null : String.valueOf(value);

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can still map to DTOs, but keep tolerant fallback for critical fields.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 5 Use idempotency to avoid duplicate writes (very common in retries)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When retries happen, you must ensure you don’t create duplicates externally.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Add an idempotency key header (if API supports it)&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;req.setHeader('Idempotency-Key', String.valueOf(recordId));&lt;br&gt;
&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Or always send a stable externalId in payload and have the external system “upsert”.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 6 Add proper monitoring (so “intermittent” becomes measurable)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Create a custom object like &lt;code&gt;Integration_Log__c&lt;/code&gt; with fields:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Operation__c&lt;/li&gt;
&lt;li&gt;Related_Record_Id__c&lt;/li&gt;
&lt;li&gt;Level__c (INFO/ERROR)&lt;/li&gt;
&lt;li&gt;Message__c&lt;/li&gt;
&lt;li&gt;Payload__c (Long Text)&lt;/li&gt;
&lt;li&gt;Status_Code__c (Number)&lt;/li&gt;
&lt;li&gt;Timestamp__c (Date/Time)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then build a simple report/dashboard:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Failures by day&lt;/li&gt;
&lt;li&gt;Top error codes (401/403/429/5xx)&lt;/li&gt;
&lt;li&gt;Records with repeated failures&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Step 7 Write stable tests using HttpCalloutMock (mandatory for deployments)&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;Mock success + failure&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
@IsTest
private class ExternalApiServiceTest {

    private class SuccessMock implements HttpCalloutMock {
        public HTTPResponse respond(HTTPRequest req) {
            HttpResponse res = new HttpResponse();
            res.setStatusCode(200);
            res.setBody('{"status":"ok","id":"ABC123"}');
            return res;
        }
    }

    private class FailureMock implements HttpCalloutMock {
        public HTTPResponse respond(HTTPRequest req) {
            HttpResponse res = new HttpResponse();
            res.setStatusCode(500);
            res.setBody('{"error":"server_error"}');
            return res;
        }
    }

    @IsTest
    static void testSendRequestSuccess() {
        Test.setMock(HttpCalloutMock.class, new SuccessMock());

        ExternalApiService.ApiResponse resp =
            ExternalApiService.sendRequest('GET', '/health', null, 5000);

        System.assertEquals(200, resp.statusCode);
        System.assertEquals('ok', String.valueOf(resp.json.get('status')));
    }

    @IsTest
    static void testSendRequestFailureThrows() {
        Test.setMock(HttpCalloutMock.class, new FailureMock());
        try {
            ExternalApiService.sendRequest('GET', '/health', null, 5000);
            System.assert(false, 'Expected exception');
        } catch (ExternalApiService.ApiException ex) {
            System.assertEquals(500, ex.statusCode);
        }
    }
}


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Conclusion&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Integration failures in Salesforce usually feel “intermittent” because the root causes are hidden auth/token issues, timeouts, rate limits (429), server errors (5xx), or small API contract changes. If callouts aren’t handled with proper status checks, timeouts, retries, and logging, a single external glitch can break your sync and create messy production incidents.&lt;br&gt;
&lt;strong&gt;To make integrations stable and predictable:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use Named Credentials so authentication and endpoints are managed        securely and consistently.&lt;/li&gt;
&lt;li&gt;Build a single callout wrapper that handles timeouts, validates HTTP status codes, and parses responses safely.&lt;/li&gt;
&lt;li&gt;Retry only transient failures (timeouts/429/5xx) using Queueable callouts, and avoid duplicates with idempotency keys/external IDs.&lt;/li&gt;
&lt;li&gt;Add structured logging + monitoring so failures are visible, traceable, and measurable.&lt;/li&gt;
&lt;li&gt;Write strong HttpCalloutMock tests so deployments don’t break and issues are caught early.&lt;/li&gt;
&lt;li&gt;With these practices, your Salesforce-to-external-system sync becomes reliable, debuggable, and production-ready, even when external APIs change or behave unpredictably.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>security</category>
      <category>api</category>
      <category>architecture</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Salesforce LWC/Aura UI Bugs and Performance Bottlenecks</title>
      <dc:creator>Selavina B</dc:creator>
      <pubDate>Sat, 13 Dec 2025 12:57:04 +0000</pubDate>
      <link>https://dev.to/selavina_b_de3b87f13c96a6/lwcaura-ui-bugs-and-performance-bottlenecks-3jbd</link>
      <guid>https://dev.to/selavina_b_de3b87f13c96a6/lwcaura-ui-bugs-and-performance-bottlenecks-3jbd</guid>
      <description>&lt;p&gt;&lt;strong&gt;Problem statement&lt;/strong&gt;&lt;br&gt;
Lightning components load slowly, fail with large datasets, or throw runtime errors due to inefficient Apex calls, poor state management, and missing error handling.&lt;/p&gt;

&lt;p&gt;Step 1  Identify the bottleneck first (quick checklist)&lt;br&gt;
Most slow/buggy Lightning UIs come from one (or more) of these:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Too many Apex calls (N+1 calls, call per row, call per keystroke)
2.Returning too much data (querying 10k+ records, too many fields)&lt;/li&gt;
&lt;li&gt;No pagination / no server-side filtering&lt;/li&gt;
&lt;li&gt;No caching (@AuraEnabled(cacheable=true) missing)&lt;/li&gt;
&lt;li&gt;Bad state management (re-render loops, heavy getters, repeated array cloning)&lt;/li&gt;
&lt;li&gt;Missing error handling (uncaught promise errors → blank UI / red error)
7.Inefficient SOQL (non-selective filters, no indexes, sorting huge sets)
We’ll fix these with a solid baseline architecture.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Step 2 Fix Apex: cacheable + paginated + minimal fields&lt;/strong&gt;&lt;br&gt;
Apex Controller (server-side pagination + search)&lt;br&gt;
This avoids loading thousands of rows and keeps the UI fast.&lt;br&gt;
ContactBrowserController.cls&lt;br&gt;
`public with sharing class ContactBrowserController {&lt;br&gt;
    public class PageResult {&lt;br&gt;
        @AuraEnabled public List records;&lt;br&gt;
        @AuraEnabled public Integer total;&lt;br&gt;
        @AuraEnabled public String nextCursor; // for keyset pagination (optional)&lt;br&gt;
    }&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@AuraEnabled(cacheable=true)
public static PageResult fetchContacts(Integer pageSize, Integer pageNumber, String searchKey) {
    pageSize = (pageSize == null || pageSize &amp;lt;= 0) ? 25 : Math.min(pageSize, 200);
    pageNumber = (pageNumber == null || pageNumber &amp;lt;= 0) ? 1 : pageNumber;

    String likeKey = String.isBlank(searchKey) ? null : ('%' + searchKey.trim() + '%');

    // Count query (for pagination UI)
    Integer totalCount;
    if (likeKey == null) {
        totalCount = [SELECT COUNT() FROM Contact WHERE IsDeleted = false];
    } else {
        totalCount = [
            SELECT COUNT()
            FROM Contact
            WHERE IsDeleted = false
            AND (Name LIKE :likeKey OR Email LIKE :likeKey)
        ];
    }

    Integer offsetRows = (pageNumber - 1) * pageSize;

    List&amp;lt;Contact&amp;gt; rows;
    if (likeKey == null) {
        rows = [
            SELECT Id, Name, Email, Phone, Account.Name
            FROM Contact
            WHERE IsDeleted = false
            ORDER BY LastModifiedDate DESC
            LIMIT :pageSize OFFSET :offsetRows
        ];
    } else {
        rows = [
            SELECT Id, Name, Email, Phone, Account.Name
            FROM Contact
            WHERE IsDeleted = false
            AND (Name LIKE :likeKey OR Email LIKE :likeKey)
            ORDER BY LastModifiedDate DESC
            LIMIT :pageSize OFFSET :offsetRows
        ];
    }

    PageResult result = new PageResult();
    result.records = rows;
    result.total = totalCount;
    result.nextCursor = null; // kept for future keyset approach
    return result;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;}`&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why this helps&lt;/strong&gt;&lt;br&gt;
• You fetch only what you need (small page).&lt;br&gt;
• You limit fields (fewer bytes, faster).&lt;br&gt;
• You cache results for identical parameters.&lt;br&gt;
Note: OFFSET can become slow at very high page numbers. If you need huge datasets, move to keyset pagination (cursor based). I can provide that too.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3  LWC: debounce search + show spinner + handle errors&lt;/strong&gt;&lt;br&gt;
LWC HTML (datatable + search + paging)&lt;br&gt;
contactBrowser.html&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;template&amp;gt;
    &amp;lt;lightning-card title="Contacts" icon-name="standard:contact"&amp;gt;
        &amp;lt;div class="slds-p-horizontal_medium slds-p-top_small"&amp;gt;
            &amp;lt;lightning-input
                type="search"
                label="Search (Name or Email)"
                value={searchKey}
                onchange={handleSearchChange}&amp;gt;
            &amp;lt;/lightning-input&amp;gt;

            &amp;lt;template if:true={isLoading}&amp;gt;
                &amp;lt;div class="slds-m-top_small"&amp;gt;
                    &amp;lt;lightning-spinner alternative-text="Loading..." size="small"&amp;gt;&amp;lt;/lightning-spinner&amp;gt;
                &amp;lt;/div&amp;gt;
            &amp;lt;/template&amp;gt;

            &amp;lt;template if:true={errorMessage}&amp;gt;
                &amp;lt;div class="slds-m-top_small slds-text-color_error"&amp;gt;
                    {errorMessage}
                &amp;lt;/div&amp;gt;
            &amp;lt;/template&amp;gt;

            &amp;lt;lightning-datatable
                key-field="Id"
                data={rows}
                columns={columns}
                hide-checkbox-column
                class="slds-m-top_small"&amp;gt;
            &amp;lt;/lightning-datatable&amp;gt;

            &amp;lt;div class="slds-m-top_small slds-grid slds-grid_align-spread slds-grid_vertical-align-center"&amp;gt;
                &amp;lt;div&amp;gt;
                    Page {pageNumber} of {totalPages} • Total: {total}
                &amp;lt;/div&amp;gt;

                &amp;lt;div class="slds-button-group" role="group"&amp;gt;
                    &amp;lt;lightning-button label="Prev" onclick={prevPage} disabled={disablePrev}&amp;gt;&amp;lt;/lightning-button&amp;gt;
                    &amp;lt;lightning-button label="Next" onclick={nextPage} disabled={disableNext}&amp;gt;&amp;lt;/lightning-button&amp;gt;
                &amp;lt;/div&amp;gt;
            &amp;lt;/div&amp;gt;
        &amp;lt;/div&amp;gt;
    &amp;lt;/lightning-card&amp;gt;
&amp;lt;/template&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;LWC JS (imperative Apex + debounce + safe state updates)&lt;br&gt;
contactBrowser.js&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { LightningElement, track } from 'lwc';
import fetchContacts from '@salesforce/apex/ContactBrowserController.fetchContacts';
import { ShowToastEvent } from 'lightning/platformShowToastEvent';

export default class ContactBrowser extends LightningElement {
    columns = [
        { label: 'Name', fieldName: 'Name' },
        { label: 'Email', fieldName: 'Email' },
        { label: 'Phone', fieldName: 'Phone' },
        { label: 'Account', fieldName: 'AccountName' }
    ];

    @track rows = [];
    total = 0;

    pageSize = 25;
    pageNumber = 1;

    searchKey = '';
    isLoading = false;
    errorMessage = '';

    debounceTimer;

    connectedCallback() {
        this.load();
    }

    get totalPages() {
        return Math.max(1, Math.ceil(this.total / this.pageSize));
    }

    get disablePrev() {
        return this.pageNumber &amp;lt;= 1 || this.isLoading;
    }

    get disableNext() {
        return this.pageNumber &amp;gt;= this.totalPages || this.isLoading;
    }

    async load() {
        this.isLoading = true;
        this.errorMessage = '';

        try {
            const res = await fetchContacts({
                pageSize: this.pageSize,
                pageNumber: this.pageNumber,
                searchKey: this.searchKey
            });

            // Flatten Account.Name for datatable
            this.rows = (res.records || []).map(r =&amp;gt; ({
                ...r,
                AccountName: r.Account ? r.Account.Name : ''
            }));
            this.total = res.total || 0;

            // Clamp pageNumber if total changed drastically
            if (this.pageNumber &amp;gt; this.totalPages) {
                this.pageNumber = this.totalPages;
            }
        } catch (e) {
            this.errorMessage = this.reduceError(e);
            this.rows = [];
            this.total = 0;

            this.dispatchEvent(
                new ShowToastEvent({
                    title: 'Error loading contacts',
                    message: this.errorMessage,
                    variant: 'error'
                })
            );
        } finally {
            this.isLoading = false;
        }
    }

    handleSearchChange(event) {
        this.searchKey = event.target.value;

        // Debounce to avoid Apex call on every keystroke
        window.clearTimeout(this.debounceTimer);
        this.debounceTimer = window.setTimeout(() =&amp;gt; {
            this.pageNumber = 1;
            this.load();
        }, 400);
    }

    nextPage() {
        if (this.pageNumber &amp;lt; this.totalPages) {
            this.pageNumber += 1;
            this.load();
        }
    }

    prevPage() {
        if (this.pageNumber &amp;gt; 1) {
            this.pageNumber -= 1;
            this.load();
        }
    }

    reduceError(e) {
        // Works for Apex + JS errors
        if (Array.isArray(e?.body)) return e.body.map(x =&amp;gt; x.message).join(', ');
        return e?.body?.message || e?.message || 'Unknown error';
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What these fixes&lt;/strong&gt;&lt;br&gt;
• Debounce prevents “Apex call per keystroke”&lt;br&gt;
• Spinner prevents “UI looks frozen”&lt;br&gt;
• Toast + error message prevents “silent failure”&lt;br&gt;
• Pagination prevents huge dataset rendering&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 4 Fix common performance killers (must-do rules)&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;Rule A: Don’t call Apex in loops&lt;/strong&gt;&lt;br&gt;
Bad: call Apex once per row&lt;br&gt;
Good: one Apex call returns all needed data for the page.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rule B: Query only needed fields&lt;/strong&gt;&lt;br&gt;
Every extra field increases payload size and JSON parse time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rule C: Prefer cacheable reads&lt;/strong&gt;&lt;br&gt;
Use:&lt;br&gt;
• @AuraEnabled(cacheable=true) for read methods&lt;br&gt;
• refreshApex() when you actually need a refresh (wired approach)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rule D: Avoid heavy getters causing re-render storms&lt;/strong&gt;&lt;br&gt;
If your getter does heavy logic or map/filter each render, move that computation to a one-time transform in load().&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 5 If you’re using Aura: add client-side caching + proper error handling&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;Aura server action caching (storable)&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Aura controller
let action = component.get("c.fetchContacts");
action.setParams({ pageSize: 25, pageNumber: 1, searchKey: '' });

// Cache response (works for cacheable methods)
action.setStorable();

action.setCallback(this, function(response){
    let state = response.getState();
    if(state === "SUCCESS"){
        component.set("v.rows", response.getReturnValue().records);
    } else {
        let errors = response.getError();
        // show toast / log
    }
});
$A.enqueueAction(action);

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 6 Add a “large data” safety net&lt;/strong&gt;&lt;br&gt;
When datasets get large, do these:&lt;br&gt;
1.Server-side pagination (already done)&lt;br&gt;
2.Limit pageSize (cap at 200)&lt;br&gt;
3.Consider keyset pagination (cursor based) instead of OFFSET for deep paging&lt;br&gt;
4.Ensure SOQL filters are selective (indexed fields where possible)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 7 Quick “production-grade” checklist&lt;/strong&gt;&lt;br&gt;
• Apex read methods are cacheable=true&lt;br&gt;
• UI uses pagination / infinite scroll (not loading all)&lt;br&gt;
• Search is debounced&lt;br&gt;
• Handle errors with toast + user-friendly message&lt;br&gt;
• Avoid repeated Apex calls; combine queries where possible&lt;br&gt;
• Flatten/transform data once (not repeatedly in getters)&lt;br&gt;
• Validate SOQL selectivity (especially in production volume)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Conclusion&lt;/strong&gt;&lt;br&gt;
LWC/Aura UI bugs and performance bottlenecks usually happen when components try to load too much data at once, make too many Apex calls, or fail to manage state and errors properly. What works fine with small sandbox data often breaks in production when record volumes grow and network/API delays increase.&lt;br&gt;
To fix this reliably:&lt;br&gt;
• Move heavy work to the server and use server-side filtering + pagination instead of loading thousands of rows.&lt;br&gt;
• Mark read-only Apex methods as @AuraEnabled(cacheable=true) to reduce repeat calls and speed up rendering.&lt;br&gt;
• Prevent unnecessary calls by using debounced search, avoiding Apex calls inside loops, and keeping the UI state minimal.&lt;br&gt;
• Always include proper error handling (try/catch, toast messages, fallback UI) so runtime failures don’t crash the component silently.&lt;br&gt;
• Query only the fields you actually need and use efficient SOQL to keep response payloads small and fast.&lt;/p&gt;

</description>
      <category>frontend</category>
      <category>performance</category>
      <category>backend</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Salesforce Sandbox vs Production Configuration Drift</title>
      <dc:creator>Selavina B</dc:creator>
      <pubDate>Wed, 10 Dec 2025 12:36:57 +0000</pubDate>
      <link>https://dev.to/selavina_b_de3b87f13c96a6/sandbox-vs-production-configuration-drift-20ej</link>
      <guid>https://dev.to/selavina_b_de3b87f13c96a6/sandbox-vs-production-configuration-drift-20ej</guid>
      <description>&lt;p&gt;Problem statement: Features that work correctly in sandbox behave differently in production because of differences in metadata, data volume, automation, or integrations, making bugs hard to reproduce and increasing deployment risk.&lt;/p&gt;

&lt;p&gt;1 Understand the types of Sandbox vs Prod Drift&lt;/p&gt;

&lt;p&gt;Most of your issues will be in 4 buckets:&lt;/p&gt;

&lt;p&gt;Metadata drift&lt;/p&gt;

&lt;p&gt;Different Flows, Validation Rules, Page Layouts, Triggers, Profiles, Permission Sets, Record Types, etc.&lt;/p&gt;

&lt;p&gt;Data drift&lt;/p&gt;

&lt;p&gt;Sandbox has tiny, clean data.&lt;/p&gt;

&lt;p&gt;Prod has large volume, nulls, weird values, old records.&lt;/p&gt;

&lt;p&gt;Automation drift&lt;/p&gt;

&lt;p&gt;Different Flows active/inactive&lt;/p&gt;

&lt;p&gt;Different scheduled jobs, batch classes running&lt;/p&gt;

&lt;p&gt;Different integrations enabled&lt;/p&gt;

&lt;p&gt;Environment-specific config&lt;/p&gt;

&lt;p&gt;Different endpoints, API keys, Named Credentials, feature toggles.&lt;/p&gt;

&lt;p&gt;We’ll attack all 4.&lt;/p&gt;

&lt;p&gt;2 Step 1 – Stop “clicking directly” in Production&lt;/p&gt;

&lt;p&gt;Hard truth:&lt;br&gt;
If admins/devs change things directly in production (Flows, fields, page layouts) that are not in sandbox → you will have drift.&lt;/p&gt;

&lt;p&gt;Policy to adopt&lt;/p&gt;

&lt;p&gt;All metadata changes:&lt;/p&gt;

&lt;p&gt;Design &amp;amp; build in sandbox&lt;/p&gt;

&lt;p&gt;Commit to Git&lt;/p&gt;

&lt;p&gt;Deploy to Prod from source/metadata (SFDX, CI, or at least change sets).&lt;/p&gt;

&lt;p&gt;Direct production-only changes = strictly forbidden (except hotfix, then back-port to sandbox).&lt;/p&gt;

&lt;p&gt;3 Step 2 – Use SFDX + Git to compare sandbox vs prod&lt;/p&gt;

&lt;p&gt;You can literally see the differences between orgs.&lt;/p&gt;

&lt;p&gt;3.1 Setup SFDX project (once)&lt;br&gt;
`# Create project&lt;br&gt;
sfdx force:project:create -n my-salesforce-project&lt;br&gt;
cd my-salesforce-project&lt;/p&gt;

&lt;h1&gt;
  
  
  Authorize orgs
&lt;/h1&gt;

&lt;p&gt;sfdx auth:web:login -a DevSandbox&lt;br&gt;
sfdx auth:web:login -a ProdOrg`&lt;/p&gt;

&lt;p&gt;3.2 Retrieve metadata from both orgs&lt;/p&gt;

&lt;p&gt;Pick important metadata types in manifest/package.xml (ApexClass, ApexTrigger, Flow, ValidationRule via CustomObject, PermissionSet, etc.)&lt;/p&gt;

&lt;p&gt;Example manifest/package.xml snippet:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&amp;lt;?xml version="1.0" encoding="UTF-8"?&amp;gt;&lt;br&gt;
&amp;lt;Package xmlns="http://soap.sforce.com/2006/04/metadata"&amp;gt;&lt;br&gt;
    &amp;lt;types&amp;gt;&lt;br&gt;
        &amp;lt;members&amp;gt;*&amp;lt;/members&amp;gt;&lt;br&gt;
        &amp;lt;name&amp;gt;ApexClass&amp;lt;/name&amp;gt;&lt;br&gt;
    &amp;lt;/types&amp;gt;&lt;br&gt;
    &amp;lt;types&amp;gt;&lt;br&gt;
        &amp;lt;members&amp;gt;*&amp;lt;/members&amp;gt;&lt;br&gt;
        &amp;lt;name&amp;gt;ApexTrigger&amp;lt;/name&amp;gt;&lt;br&gt;
    &amp;lt;/types&amp;gt;&lt;br&gt;
    &amp;lt;types&amp;gt;&lt;br&gt;
        &amp;lt;members&amp;gt;*&amp;lt;/members&amp;gt;&lt;br&gt;
        &amp;lt;name&amp;gt;Flow&amp;lt;/name&amp;gt;&lt;br&gt;
    &amp;lt;/types&amp;gt;&lt;br&gt;
    &amp;lt;types&amp;gt;&lt;br&gt;
        &amp;lt;members&amp;gt;*&amp;lt;/members&amp;gt;&lt;br&gt;
        &amp;lt;name&amp;gt;Profile&amp;lt;/name&amp;gt;&lt;br&gt;
    &amp;lt;/types&amp;gt;&lt;br&gt;
    &amp;lt;types&amp;gt;&lt;br&gt;
        &amp;lt;members&amp;gt;*&amp;lt;/members&amp;gt;&lt;br&gt;
        &amp;lt;name&amp;gt;PermissionSet&amp;lt;/name&amp;gt;&lt;br&gt;
    &amp;lt;/types&amp;gt;&lt;br&gt;
    &amp;lt;types&amp;gt;&lt;br&gt;
        &amp;lt;members&amp;gt;*&amp;lt;/members&amp;gt;&lt;br&gt;
        &amp;lt;name&amp;gt;CustomObject&amp;lt;/name&amp;gt;&lt;br&gt;
    &amp;lt;/types&amp;gt;&lt;br&gt;
    &amp;lt;version&amp;gt;59.0&amp;lt;/version&amp;gt;&lt;br&gt;
&amp;lt;/Package&amp;gt;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Then:&lt;/p&gt;

&lt;p&gt;`# Retrieve from sandbox&lt;br&gt;
sfdx force:source:retrieve -x manifest/package.xml -u DevSandbox -w 10&lt;/p&gt;

&lt;h1&gt;
  
  
  Copy this folder (or branch) as "prod" version:
&lt;/h1&gt;

&lt;p&gt;git checkout -b sandbox-version&lt;br&gt;
git add .&lt;br&gt;
git commit -m "Sandbox metadata snapshot"&lt;/p&gt;

&lt;h1&gt;
  
  
  Retrieve from production (in another branch)
&lt;/h1&gt;

&lt;p&gt;git checkout -b prod-version&lt;br&gt;
sfdx force:source:retrieve -x manifest/package.xml -u ProdOrg -w 10&lt;br&gt;
git add .&lt;br&gt;
git commit -m "Production metadata snapshot"`&lt;/p&gt;

&lt;p&gt;Now you can:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;git diff sandbox-version..prod-version&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;…and literally see which Flows, Validation Rules, Triggers, Profiles differ.&lt;/p&gt;

&lt;p&gt;This shows you where behavior differs.&lt;/p&gt;

&lt;p&gt;4 Step 3 – Handle environment-specific configuration with Custom Metadata&lt;/p&gt;

&lt;p&gt;Don’t hardcode “sandbox vs prod” in Apex. Use Custom Metadata / Custom Settings.&lt;/p&gt;

&lt;p&gt;4.1 Create a Custom Metadata Type (UI)&lt;/p&gt;

&lt;p&gt;Name: &lt;code&gt;Environment_Config__mdt&lt;/code&gt;&lt;br&gt;
Fields:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Environment_Name__c&lt;/code&gt; (Text)&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Base_API_URL__c&lt;/code&gt;(Text)&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Is_FeatureX_Enabled__c&lt;/code&gt; (Checkbox)&lt;/p&gt;

&lt;p&gt;Create one record per org:&lt;/p&gt;

&lt;p&gt;In sandbox:&lt;/p&gt;

&lt;p&gt;Environment_Name__c = &lt;code&gt;SANDBOX&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Base_API_URL__c = &lt;code&gt;https://sandbox-api.example.com&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Is_FeatureX_Enabled__c = &lt;code&gt;true&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;In prod:&lt;/p&gt;

&lt;p&gt;Environment_Name__c = &lt;code&gt;PROD&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Base_API_URL__c =&lt;code&gt;https://api.example.com&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Is_FeatureX_Enabled__c = &lt;code&gt;false&lt;/code&gt; (or as needed)&lt;/p&gt;

&lt;p&gt;4.2 Apex helper to read config&lt;br&gt;
`public with sharing class EnvConfigService {&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;private static Environment_Config__mdt config;

private static Environment_Config__mdt getConfig() {
    if (config == null) {
        config = [
            SELECT Environment_Name__c, Base_API_URL__c, Is_FeatureX_Enabled__c
            FROM Environment_Config__mdt
            LIMIT 1
        ];
    }
    return config;
}

public static String getBaseApiUrl() {
    return getConfig().Base_API_URL__c;
}

public static Boolean isFeatureXEnabled() {
    return getConfig().Is_FeatureX_Enabled__c;
}

public static String getEnvironmentName() {
    return getConfig().Environment_Name__c;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;}&lt;br&gt;
&lt;code&gt;&lt;br&gt;
4.3 Use it in your logic&lt;br&gt;
&lt;/code&gt;public with sharing class ExternalSyncService {&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public static void sendOrderToExternalSystem(Id orderId) {
    String baseUrl = EnvConfigService.getBaseApiUrl();
    if (String.isBlank(baseUrl)) {
        // Log &amp;amp; skip in misconfigured orgs
        System.debug('Base API URL not configured');
        return;
    }

    // callout code here ...
    // HttpRequest req = new HttpRequest();
    // req.setEndpoint(baseUrl + '/orders/' + orderId);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;}`&lt;/p&gt;

&lt;p&gt;Now:&lt;/p&gt;

&lt;p&gt;Sandbox and production can use different endpoints, toggles, etc.&lt;/p&gt;

&lt;p&gt;You do NOT need to if/else on System.URL.getOrgDomainUrl() or similar hacks.&lt;/p&gt;

&lt;p&gt;5 Step 4 – Align automation (Flows, Scheduled Jobs, Triggers)&lt;/p&gt;

&lt;p&gt;A very common source of drift:&lt;/p&gt;

&lt;p&gt;Flow active in sandbox, inactive in prod (or vice versa)&lt;/p&gt;

&lt;p&gt;Scheduled job running in prod but not sandbox&lt;/p&gt;

&lt;p&gt;Old trigger still active in prod but removed/disabled in sandbox&lt;/p&gt;

&lt;p&gt;5.1 See scheduled jobs with Apex&lt;/p&gt;

&lt;p&gt;Run this in Execute Anonymous (both orgs):&lt;/p&gt;

&lt;p&gt;`List jobs = [&lt;br&gt;
    SELECT Id, CronJobDetail.Name, State, NextFireTime&lt;br&gt;
    FROM CronTrigger&lt;br&gt;
    ORDER BY NextFireTime&lt;br&gt;
];&lt;/p&gt;

&lt;p&gt;for (CronTrigger ct : jobs) {&lt;br&gt;
    System.debug('Job: ' + ct.CronJobDetail.Name + &lt;br&gt;
                 ' | State: ' + ct.State + &lt;br&gt;
                 ' | NextFire: ' + ct.NextFireTime);&lt;br&gt;
}`&lt;/p&gt;

&lt;p&gt;Compare sandbox vs production list → you’ll see which scheduled jobs differ.&lt;/p&gt;

&lt;p&gt;5.2 Flow &amp;amp; Trigger status&lt;/p&gt;

&lt;p&gt;These are metadata; best is to compare via SFDX / Git (Step 2). Key things:&lt;/p&gt;

&lt;p&gt;For Flows:&lt;/p&gt;

&lt;p&gt;Same versions should be Active in both orgs.&lt;/p&gt;

&lt;p&gt;For Apex Triggers:&lt;/p&gt;

&lt;p&gt;Same triggers should be Active/Inactive consistently.&lt;/p&gt;

&lt;p&gt;If sandbox has a newer active Flow version than prod, then of course behavior differs.&lt;/p&gt;

&lt;p&gt;6 Step 5 – Use realistic data sets (data volume + edge cases)&lt;/p&gt;

&lt;p&gt;In sandbox, things work with 10 test records. In prod, there are 1M records, null fields, weird values.&lt;/p&gt;

&lt;p&gt;Solution: seed sandbox with better data.&lt;/p&gt;

&lt;p&gt;Option A – Use Full / Partial sandbox&lt;/p&gt;

&lt;p&gt;Prefer at least one Partial or Full sandbox where data is closer to prod.&lt;/p&gt;

&lt;p&gt;Option B – Seed data via script or test factory&lt;/p&gt;

&lt;p&gt;Small Apex script to create “heavy” data scenario:&lt;/p&gt;

&lt;p&gt;`// Execute Anonymous in sandbox&lt;/p&gt;

&lt;p&gt;Account parent = new Account(Name = 'Parent Account');&lt;br&gt;
insert parent;&lt;/p&gt;

&lt;p&gt;List manyAccs = new List();&lt;br&gt;
for (Integer i = 0; i &amp;lt; 5000; i++) {&lt;br&gt;
    manyAccs.add(new Account(&lt;br&gt;
        Name = 'Test Account ' + i,&lt;br&gt;
        ParentId = parent.Id,&lt;br&gt;
        Industry = (i % 2 == 0) ? 'Banking' : null // some nulls&lt;br&gt;
    ));&lt;br&gt;
}&lt;br&gt;
insert manyAccs;`&lt;/p&gt;

&lt;p&gt;This will help reveal performance or logic issues that only appear at scale.&lt;/p&gt;

&lt;p&gt;7 Step 6 – Add environment-aware logging to debug differences&lt;/p&gt;

&lt;p&gt;When behavior differs, you want quick visibility in prod without relying only on debug logs.&lt;/p&gt;

&lt;p&gt;7.1 Create a simple Log__c object&lt;/p&gt;

&lt;p&gt;Custom Object: &lt;code&gt;Log__c&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Fields:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Level__c&lt;/code&gt; (Picklist: INFO, WARN, ERROR)&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Location__c&lt;/code&gt; (Text) – class/method name&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Message__c&lt;/code&gt; (Long Text)&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Context__c&lt;/code&gt; (Long Text) – JSON of important variables&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Environment__c&lt;/code&gt; (Formula) referencing config or &lt;code&gt;OrganizationType&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;7.2 Apex logging helper&lt;br&gt;
`public with sharing class LogService {&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public static void info(String location, String message, String contextJson) {
    insertLog('INFO', location, message, contextJson);
}

public static void error(String location, String message, String contextJson) {
    insertLog('ERROR', location, message, contextJson);
}

private static void insertLog(String level, String location, String message, String contextJson) {
    try {
        Log__c log = new Log__c(
            Level__c = level,
            Location__c = location,
            Message__c = message,
            Context__c = contextJson
        );
        insert log;
    } catch (Exception e) {
        // Avoid recursive logging failures
        System.debug('Failed to insert log: ' + e.getMessage());
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;}`&lt;/p&gt;

&lt;p&gt;7.3 Use it in critical code paths&lt;br&gt;
`public with sharing class OrderService {&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public static void processOrder(Id orderId) {
    try {
        Order__c ord = [
            SELECT Id, Status__c, Amount__c
            FROM Order__c
            WHERE Id = :orderId
        ];

        LogService.info(
            'OrderService.processOrder',
            'Starting order processing',
            JSON.serialize(ord)
        );

        // ... processing logic ...

    } catch (Exception e) {
        LogService.error(
            'OrderService.processOrder',
            'Error while processing order: ' + e.getMessage(),
            '{"orderId":"' + String.valueOf(orderId) + '"}'
        );
        throw e;
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;}`&lt;/p&gt;

&lt;p&gt;Now you can compare logs from sandbox vs prod to see exactly where behavior diverges.&lt;/p&gt;

&lt;p&gt;8 Step 7 – Add a “pre-deployment checklist” for drift&lt;/p&gt;

&lt;p&gt;Before any deploy:&lt;/p&gt;

&lt;p&gt;Metadata alignment&lt;/p&gt;

&lt;p&gt;SFDX diff for key components: Flows, Triggers, Classes, Validation Rules, Permission Sets.&lt;/p&gt;

&lt;p&gt;Automation alignment&lt;/p&gt;

&lt;p&gt;Same scheduled jobs?&lt;/p&gt;

&lt;p&gt;Same active Flow versions?&lt;/p&gt;

&lt;p&gt;Environment config&lt;/p&gt;

&lt;p&gt;Custom Metadata / Named Credentials set correctly?&lt;/p&gt;

&lt;p&gt;Smoke tests&lt;/p&gt;

&lt;p&gt;Run a small script in sandbox and prod (read/write a test record, call critical logic) and compare behavior/logs.&lt;/p&gt;

&lt;p&gt;Even a simple Apex smoke test method helps:&lt;/p&gt;

&lt;p&gt;`@IsTest&lt;br&gt;
private class SmokeTest {&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@IsTest
static void testCoreLogic() {
    // create data, call main service methods, assert no exceptions
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;}`&lt;/p&gt;

&lt;p&gt;Run this in every org.&lt;/p&gt;

&lt;p&gt;Conclusion – Controlling Sandbox vs Production Drift&lt;/p&gt;

&lt;p&gt;Your problem isn’t random:&lt;/p&gt;

&lt;p&gt;Features behave differently in sandbox and production when metadata, data, automation, and environment config drift apart.&lt;/p&gt;

&lt;p&gt;To fix this reliably:&lt;/p&gt;

&lt;p&gt;Centralize metadata in Git + SFDX and deploy from source, not random clicks.&lt;/p&gt;

&lt;p&gt;Use Custom Metadata/Settings for environment-specific URLs, flags, and config.&lt;/p&gt;

&lt;p&gt;Keep Flows, Triggers, and scheduled jobs aligned using metadata diffs and org comparisons.&lt;/p&gt;

&lt;p&gt;Seed sandbox with realistic data to catch scale and null issues earlier.&lt;/p&gt;

&lt;p&gt;Add logging + a pre-deployment checklist so you can see and prevent differences before they reach users.&lt;/p&gt;

</description>
      <category>cicd</category>
      <category>devops</category>
      <category>testing</category>
    </item>
  </channel>
</rss>
