DEV Community

Selavina B
Selavina B

Posted on

Sandbox vs Production Configuration Drift

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.

1 Understand the types of Sandbox vs Prod Drift

Most of your issues will be in 4 buckets:

Metadata drift

Different Flows, Validation Rules, Page Layouts, Triggers, Profiles, Permission Sets, Record Types, etc.

Data drift

Sandbox has tiny, clean data.

Prod has large volume, nulls, weird values, old records.

Automation drift

Different Flows active/inactive

Different scheduled jobs, batch classes running

Different integrations enabled

Environment-specific config

Different endpoints, API keys, Named Credentials, feature toggles.

We’ll attack all 4.

2 Step 1 – Stop “clicking directly” in Production

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

Policy to adopt

All metadata changes:

Design & build in sandbox

Commit to Git

Deploy to Prod from source/metadata (SFDX, CI, or at least change sets).

Direct production-only changes = strictly forbidden (except hotfix, then back-port to sandbox).

3 Step 2 – Use SFDX + Git to compare sandbox vs prod

You can literally see the differences between orgs.

3.1 Setup SFDX project (once)
`# Create project
sfdx force:project:create -n my-salesforce-project
cd my-salesforce-project

Authorize orgs

sfdx auth:web:login -a DevSandbox
sfdx auth:web:login -a ProdOrg`

3.2 Retrieve metadata from both orgs

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

Example manifest/package.xml snippet:

<?xml version="1.0" encoding="UTF-8"?>
<Package xmlns="http://soap.sforce.com/2006/04/metadata">
<types>
<members>*</members>
<name>ApexClass</name>
</types>
<types>
<members>*</members>
<name>ApexTrigger</name>
</types>
<types>
<members>*</members>
<name>Flow</name>
</types>
<types>
<members>*</members>
<name>Profile</name>
</types>
<types>
<members>*</members>
<name>PermissionSet</name>
</types>
<types>
<members>*</members>
<name>CustomObject</name>
</types>
<version>59.0</version>
</Package>

Then:

`# Retrieve from sandbox
sfdx force:source:retrieve -x manifest/package.xml -u DevSandbox -w 10

Copy this folder (or branch) as "prod" version:

git checkout -b sandbox-version
git add .
git commit -m "Sandbox metadata snapshot"

Retrieve from production (in another branch)

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

Now you can:

git diff sandbox-version..prod-version

…and literally see which Flows, Validation Rules, Triggers, Profiles differ.

This shows you where behavior differs.

4 Step 3 – Handle environment-specific configuration with Custom Metadata

Don’t hardcode “sandbox vs prod” in Apex. Use Custom Metadata / Custom Settings.

4.1 Create a Custom Metadata Type (UI)

Name: Environment_Config__mdt
Fields:

Environment_Name__c (Text)

Base_API_URL__c(Text)

Is_FeatureX_Enabled__c (Checkbox)

Create one record per org:

In sandbox:

Environment_Name__c = SANDBOX

Base_API_URL__c = https://sandbox-api.example.com

Is_FeatureX_Enabled__c = true

In prod:

Environment_Name__c = PROD

Base_API_URL__c =https://api.example.com

Is_FeatureX_Enabled__c = false (or as needed)

4.2 Apex helper to read config
`public with sharing class EnvConfigService {

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;
}
Enter fullscreen mode Exit fullscreen mode

}

4.3 Use it in your logic
public with sharing class ExternalSyncService {

public static void sendOrderToExternalSystem(Id orderId) {
    String baseUrl = EnvConfigService.getBaseApiUrl();
    if (String.isBlank(baseUrl)) {
        // Log & skip in misconfigured orgs
        System.debug('Base API URL not configured');
        return;
    }

    // callout code here ...
    // HttpRequest req = new HttpRequest();
    // req.setEndpoint(baseUrl + '/orders/' + orderId);
}
Enter fullscreen mode Exit fullscreen mode

}`

Now:

Sandbox and production can use different endpoints, toggles, etc.

You do NOT need to if/else on System.URL.getOrgDomainUrl() or similar hacks.

5 Step 4 – Align automation (Flows, Scheduled Jobs, Triggers)

A very common source of drift:

Flow active in sandbox, inactive in prod (or vice versa)

Scheduled job running in prod but not sandbox

Old trigger still active in prod but removed/disabled in sandbox

5.1 See scheduled jobs with Apex

Run this in Execute Anonymous (both orgs):

`List jobs = [
SELECT Id, CronJobDetail.Name, State, NextFireTime
FROM CronTrigger
ORDER BY NextFireTime
];

for (CronTrigger ct : jobs) {
System.debug('Job: ' + ct.CronJobDetail.Name +
' | State: ' + ct.State +
' | NextFire: ' + ct.NextFireTime);
}`

Compare sandbox vs production list → you’ll see which scheduled jobs differ.

5.2 Flow & Trigger status

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

For Flows:

Same versions should be Active in both orgs.

For Apex Triggers:

Same triggers should be Active/Inactive consistently.

If sandbox has a newer active Flow version than prod, then of course behavior differs.

6 Step 5 – Use realistic data sets (data volume + edge cases)

In sandbox, things work with 10 test records. In prod, there are 1M records, null fields, weird values.

Solution: seed sandbox with better data.

Option A – Use Full / Partial sandbox

Prefer at least one Partial or Full sandbox where data is closer to prod.

Option B – Seed data via script or test factory

Small Apex script to create “heavy” data scenario:

`// Execute Anonymous in sandbox

Account parent = new Account(Name = 'Parent Account');
insert parent;

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

This will help reveal performance or logic issues that only appear at scale.

7 Step 6 – Add environment-aware logging to debug differences

When behavior differs, you want quick visibility in prod without relying only on debug logs.

7.1 Create a simple Log__c object

Custom Object: Log__c

Fields:

Level__c (Picklist: INFO, WARN, ERROR)

Location__c (Text) – class/method name

Message__c (Long Text)

Context__c (Long Text) – JSON of important variables

Environment__c (Formula) referencing config or OrganizationType

7.2 Apex logging helper
`public with sharing class LogService {

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());
    }
}
Enter fullscreen mode Exit fullscreen mode

}`

7.3 Use it in critical code paths
`public with sharing class OrderService {

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

}`

Now you can compare logs from sandbox vs prod to see exactly where behavior diverges.

8 Step 7 – Add a “pre-deployment checklist” for drift

Before any deploy:

Metadata alignment

SFDX diff for key components: Flows, Triggers, Classes, Validation Rules, Permission Sets.

Automation alignment

Same scheduled jobs?

Same active Flow versions?

Environment config

Custom Metadata / Named Credentials set correctly?

Smoke tests

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

Even a simple Apex smoke test method helps:

`@IsTest
private class SmokeTest {

@IsTest
static void testCoreLogic() {
    // create data, call main service methods, assert no exceptions
}
Enter fullscreen mode Exit fullscreen mode

}`

Run this in every org.

Conclusion – Controlling Sandbox vs Production Drift

Your problem isn’t random:

Features behave differently in sandbox and production when metadata, data, automation, and environment config drift apart.

To fix this reliably:

Centralize metadata in Git + SFDX and deploy from source, not random clicks.

Use Custom Metadata/Settings for environment-specific URLs, flags, and config.

Keep Flows, Triggers, and scheduled jobs aligned using metadata diffs and org comparisons.

Seed sandbox with realistic data to catch scale and null issues earlier.

Add logging + a pre-deployment checklist so you can see and prevent differences before they reach users.

Top comments (0)