An API request times out, but Salesforce may have already processed it. Your integration retries the request, and suddenly the same business transaction appears twice.
This is a common integration-design problem. It becomes harder when multiple workers process the same event or external systems resend requests.
It provides External ID fields and upsert operations to simplify record synchronization. However, upsert alone does not guarantee safe processing under concurrent requests.
In this article, we examine how to design Salesforce integration solutions that handle retries, duplicate events, and concurrent updates. We will use REST API examples and discuss the trade-offs between synchronous requests, queues, and asynchronous processing.
Context and Setup
A typical Crm integration connects an external application with Accounts, Contacts, Orders, or custom objects.
Consider an order-management application sending order updates to it.
Three conditions can create duplicate-write problems:
- The external application retries after a network timeout.
- Two workers process the same event simultaneously.
- A message broker redelivers an event after an acknowledgment failure. It supports External ID-based upsert operations. According to its developer documentation, Bulk API 2.0 is a suitable candidate for operations involving more than 2,000 records.
This provides a useful architectural distinction: small transactional updates and large data synchronization jobs should not necessarily follow the same processing strategy.
Solution: Build Retry-Safe Integrations
The objective is to make repeated delivery of the same business event safe without silently overwriting unrelated updates.
Step 1: Assign a Stable External ID
Every synchronized business record should have a stable identifier originating from the source system.
For example, create an External_Order_Id__c field on the Salesforce Order object.
Configure it as:
- External ID: Enabled
- Unique: Enabled
- Required: Where business rules permit This gives the integration a consistent identifier instead of relying on Salesforce-generated record IDs.
REST API supports PATCH-based upsert using an External ID field. It creates a record when no match exists and updates an existing record when one match exists.
Step 2: Use Upsert Instead of Blind Inserts
The following Node.js example sends an order through the REST API.
async function syncOrder(order, accessToken, instanceUrl) {
// Use the source-system ID to identify the record.
const externalId = encodeURIComponent(order.externalOrderId);
const url =
${instanceUrl}/services/data/vXX.X/sobjects/ +
Order__c/External_Order_Id__c/${externalId};
const response = await fetch(url, {
method: "PATCH",
headers: {
Authorization: Bearer ${accessToken},
"Content-Type": "application/json"
},
body: JSON.stringify({
Status_c: order.status,
Amount_c: order.amount
})
});
// Surface failures instead of silently accepting unsuccessful writes.
if (!response.ok) {
throw new Error(Salesforce sync failed: ${response.status});
}
return response.status;
}
The example assumes authentication and token refresh happen outside the function.
A repeated request with the same External ID targets the same record. However, concurrent requests can still create race conditions or overwrite newer values.
Therefore, record identity and event ordering require separate controls.
Step 3: Control Concurrent Requests
Serialize updates for the same external record when multiple workers can process overlapping events.
A practical architecture uses a queue partitioned by the source-system record ID.
const queues = new Map();
async function processOrderEvent(order, token, instanceUrl) {
const key = order.externalOrderId;
// Keep updates for the same order in sequence.
const previous = queues.get(key) || Promise.resolve();
const current = previous
.catch(() => {}) // Allow later events after a failed request.
.then(() => syncOrder(order, token, instanceUrl));
queues.set(key, current);
try {
return await current;
} finally {
// Remove only the completed queue entry.
if (queues.get(key) === current) {
queues.delete(key);
}
}
}
This example provides in-process serialization, not distributed locking. Multiple application instances need a shared queue or partitioning mechanism.
For higher-volume workloads, consider a durable message queue, retry policies, dead-letter handling, and event-version checks.
The trade-off is operational complexity. Synchronous processing is simpler for low-volume updates. Queues provide better control when event volume and concurrency increase.
Real-World Application
The same integration considerations matter when it supports multiple business processes and external reporting systems.
In our Rise and Shine / ASUAire Salesforce engagement at Oodles, the documented scope included multi-region CRM customization, custom objects, automated sales workflows, and external reporting integrations.
The architecture required consideration of how CRM data would move between operational workflows and connected reporting systems.
Our implementation scope covered CRM customization and integration requirements. The available project documentation does not establish a specific duplicate-request incident or measured API performance improvement. We therefore do not attribute an invented response-time reduction to this project.
For similar integration requirements, Oodles approaches data synchronization by defining record identity, ownership, update behavior, and failure handling before implementation.
Conclusion: Design for Repeated Delivery
Retry handling should be part of the integration architecture, not an afterthought added after production failures.
For integrations, focus on these five engineering decisions:
- Use unique External ID fields to identify records across systems.
- Prefer upsert over blind inserts for synchronization workflows.
- Serialize updates when multiple workers can modify the same record.
- Track event versions to prevent older events from overwriting newer data.
- Use Bulk API 2.0 for suitable large-volume operations and monitor failed records. A successful API response is not the only measure of integration correctness. The system must also handle retries, concurrency, and partial failures predictably.
Have questions about API integration, retry handling, or synchronization architecture? Contact our Salesforce experts to discuss your implementation requirements. Share your technical questions in the comments as well.
What is an External ID in Salesforce?
An External ID is a field that stores a record identifier from another system. Integrations use it to locate it's records without requiring Salesforce-generated IDs. External IDs support synchronization, upsert operations, and matching records across connected applications.How does upsert work in Salesforce REST API?
REST API upsert uses an External ID to determine whether a record already exists. It creates a record when no matching record exists and updates the matching record otherwise. Multiple matching records produce an error instead of an update.How can developers prevent duplicate records in Salesforce integrations?
Developers should use unique External ID fields, upsert operations, and stable source-system identifiers. They should also control concurrent requests for identical records. Distributed integrations need durable event tracking and appropriate retry handling.When should developers use Salesforce Bulk API 2.0?
Bulk API 2.0 suits large data operations involving thousands of records. It identifies operations exceeding 2,000 records as good candidates. It supports asynchronous insert, update, upsert, query, and delete operations, making it useful for large synchronization workloads.Does Salesforce upsert guarantee idempotency?
Upsert helps make repeated requests target the same record, but it does not guarantee complete application-level idempotency. Concurrent requests, duplicate external identifiers, and out-of-order updates still require additional controls, such as event versioning and request serialization.
Top comments (0)