Skip to main content

Bidirectional Field Synchronization with Actor-Type Guards

Overview​

When two fields represent the same value but are updated from different sources -- such as an integration system and a UI user -- keeping them in sync without creating infinite loops requires careful design. This article introduces the actor-type guard pattern, which determines the sync direction based on who is making the change rather than what changed.

  • Why it matters: Naive bidirectional sync logic causes recursive triggers, data corruption, or contradictory field values. This is a common pitfall in integration-heavy Salesforce orgs that can lead to production incidents.
  • What you will learn: How to identify the actor (integration user vs. UI user) at runtime, apply directional sync rules that prevent circular dependencies, and test both directions independently.

In integration-heavy orgs, it is common for two fields to represent the same concept from different directions. For example, Supplier_Price__c is set by an inbound integration from a procurement system, while Quoted_Unit_Price__c is set by a sales user in the UI. When one changes, the other should update to stay in sync. Without careful design, this creates an infinite loop.

The Circular Dependency Problem​

Consider a before-update trigger that syncs two fields bidirectionally:

// Naive implementation -- DO NOT USE
if (newRecord.Supplier_Price__c != oldRecord.Supplier_Price__c) {
newRecord.Quoted_Unit_Price__c = newRecord.Supplier_Price__c;
}
if (newRecord.Quoted_Unit_Price__c != oldRecord.Quoted_Unit_Price__c) {
newRecord.Supplier_Price__c = newRecord.Quoted_Unit_Price__c;
}

When the integration updates Supplier_Price__c, the trigger sets Quoted_Unit_Price__c. Because Quoted_Unit_Price__c is now different from its old value, the second condition fires and overwrites Supplier_Price__c. In an after-update trigger performing DML, this would cause recursive execution. Even in a before-update trigger (no re-trigger), the logic contradicts itself.

The Solution: Actor-Type Guards​

The key insight is that the sync direction depends on who is making the change. An inbound integration should propagate values in one direction; a UI user should propagate in the other.

Identifying the Actor​

Use a dedicated integration user for all API-based operations. Then check the running user in the trigger handler:

public class SyncGuard {

private static final Id INTEGRATION_USER_ID;

static {
// Cache the integration user's ID
List<User> integrationUsers = [
SELECT Id FROM User
WHERE Username = 'integration@acmecorp.com'
LIMIT 1
];
INTEGRATION_USER_ID = integrationUsers.isEmpty() ? null : integrationUsers[0].Id;
}

public static Boolean isIntegrationContext() {
return UserInfo.getUserId() == INTEGRATION_USER_ID;
}
}

Applying Directional Sync Rules​

With the actor identified, apply sync rules scoped by direction:

public class QuoteLineItemSyncHandler {

public static void syncPricingFields(
List<QuoteLineItem> newRecords,
Map<Id, QuoteLineItem> oldMap
) {
Boolean isIntegration = SyncGuard.isIntegrationContext();

for (QuoteLineItem qli : newRecords) {
QuoteLineItem oldQli = oldMap.get(qli.Id);

if (isIntegration) {
// Inbound integration: Supplier_Price drives Quoted_Unit_Price
if (qli.Supplier_Price__c != oldQli.Supplier_Price__c) {
qli.Quoted_Unit_Price__c = qli.Supplier_Price__c;
}
} else {
// UI user: Quoted_Unit_Price drives Supplier_Price
if (qli.Quoted_Unit_Price__c != oldQli.Quoted_Unit_Price__c) {
qli.Supplier_Price__c = qli.Quoted_Unit_Price__c;
}
}
}
}
}

This approach guarantees that only one sync direction executes per transaction, eliminating the circular dependency entirely.

Alternative: Custom Header Flag​

If multiple integration users exist or the check needs more granularity, use a static variable flag set by the integration entry point:

public class IntegrationContext {
public static Boolean isInboundSync = false;
}

The integration's REST endpoint or trigger entry point sets IntegrationContext.isInboundSync = true before performing DML. The sync handler checks this flag instead of the user ID.

Testing Both Directions​

Test classes must validate both sync directions independently:

@IsTest
static void testIntegrationContextSyncsSupplierToQuoted() {
// Run as integration user
User integrationUser = [SELECT Id FROM User WHERE Username = 'integration@acmecorp.com'];
System.runAs(integrationUser) {
QuoteLineItem qli = createTestQLI();
qli.Supplier_Price__c = 42.50;
update qli;

qli = [SELECT Quoted_Unit_Price__c FROM QuoteLineItem WHERE Id = :qli.Id];
System.assertEquals(42.50, qli.Quoted_Unit_Price__c,
'Integration update to Supplier_Price should sync to Quoted_Unit_Price');
}
}

@IsTest
static void testUIContextSyncsQuotedToSupplier() {
// Run as standard user (non-integration)
User salesUser = TestDataFactory.createStandardUser();
System.runAs(salesUser) {
QuoteLineItem qli = createTestQLI();
qli.Quoted_Unit_Price__c = 38.75;
update qli;

qli = [SELECT Supplier_Price__c FROM QuoteLineItem WHERE Id = :qli.Id];
System.assertEquals(38.75, qli.Supplier_Price__c,
'UI update to Quoted_Unit_Price should sync to Supplier_Price');
}
}

Edge Cases​

  • Admin edits both fields simultaneously: When both fields change in the same transaction, the actor-type guard still applies only one direction. Document this behavior so admins understand that the integration direction or UI direction wins based on who they are logged in as.
  • Data loader operations: Data loader runs as the logged-in user. If an admin bulk-loads data that should follow integration rules, they should either use the integration user's credentials or set the static flag.
  • Flow and Process Builder: Automated updates triggered by Flows run as the user who initiated the transaction, so the guard correctly routes sync direction based on the original actor.

Key Takeaway​

Bidirectional sync is safe when each transaction knows who is making the change and applies sync rules in only one direction. The actor-type guard -- whether based on User ID, a static flag, or a custom setting -- is the architectural boundary that prevents circular dependencies.