I have spent years building Salesforce integrations and the question I get most often is not βhow do I call an API?β β it is βwhich pattern should I use?β The answer depends entirely on three factors: how quickly you need a response, what happens when the integration fails, and who initiates the interaction.
Getting this decision right determines whether your integration is maintainable, scalable, and resilient. Getting it wrong means redesigning under pressure when volumes increase.
The Four Patterns at a Glance
Pattern 1: Request-Reply (Synchronous Callout)
Use this pattern when your business process cannot continue without a response from the external system. A credit check before order approval, an address validation before saving, a real-time inventory lookup during quoting β these all require a synchronous response.
public class CreditCheckService {
public static CreditResult checkCredit(String accountId, Decimal amount) {
HttpRequest req = new HttpRequest();
req.setEndpoint('callout:Credit_API/check');
req.setMethod('POST');
req.setHeader('Content-Type', 'application/json');
req.setTimeout(10000); // 10 second timeout β not 120s default
Map<String, Object> payload = new Map<String, Object>{
'accountId' => accountId,
'requestedAmount' => amount
};
req.setBody(JSON.serialize(payload));
Http http = new Http();
HttpResponse res = http.send(req);
if (res.getStatusCode() == 200) {
return (CreditResult) JSON.deserialize(res.getBody(), CreditResult.class);
} else {
throw new CalloutException('Credit check failed: ' + res.getStatus());
}
}
}- Set explicit timeouts shorter than the 120-second maximum. A 120-second wait freezes the userβs browser during synchronous callouts.
- Callouts cannot be made after DML in the same transaction. Use
@future(callout=true)or Queueable to work around this. - Named Credentials (
callout:Credit_API) handle authentication and endpoint management β always use them instead of hardcoded URLs.
Pattern 2: Fire-and-Forget (Platform Events)
This is the pattern I reach for when the integration operation does not need to block the userβs transaction. Order confirmations to ERP, audit log writes to data warehouses, notifications to external systems β all of these can tolerate a few seconds of latency.
Platform Events are Salesforceβs event bus. Publishers fire events, subscribers process them asynchronously. The key advantage over @future methods is replay: Salesforce retains published events for 72 hours, allowing subscribers to replay missed events after an outage.
Publishing Events
public class OrderService {
public static void confirmOrder(Order__c order) {
// Publish event β does not block the transaction
Order_Confirmed__e event = new Order_Confirmed__e(
Order_Id__c = order.Id,
Account_Id__c = order.Account__c,
Total_Amount__c = order.Total_Amount__c,
Confirmed_At__c = DateTime.now()
);
Database.SaveResult result = EventBus.publish(event);
if (!result.isSuccess()) {
// Log failure β don't throw, this is fire-and-forget
System.debug('Event publish failed: ' + result.getErrors());
}
}
}Subscribing with a Trigger
trigger OrderConfirmedTrigger on Order_Confirmed__e (after insert) {
List<Order_Confirmed__e> events = Trigger.new;
// Process asynchronously β this trigger fires in its own transaction
List<External_Order__c> externalOrders = new List<External_Order__c>();
for (Order_Confirmed__e evt : events) {
externalOrders.add(new External_Order__c(
Salesforce_Order_Id__c = evt.Order_Id__c,
Amount__c = evt.Total_Amount__c,
Status__c = 'Confirmed'
));
}
insert externalOrders;
}Pattern 3: Batch Data Sync
When you need to synchronize large volumes of data β hundreds of thousands or millions of records β neither synchronous callouts nor event-driven approaches scale. Batch Apex combined with Scheduled Jobs is the right pattern.
global class ERP_SyncBatch implements Database.Batchable<SObject>, Database.AllowsCallouts {
global Database.QueryLocator start(Database.BatchableContext bc) {
// Only sync records modified since last successful run
DateTime lastSync = getLastSyncTimestamp();
return Database.getQueryLocator(
'SELECT Id, Name, Amount__c, Status__c ' +
'FROM Order__c ' +
'WHERE LastModifiedDate > :lastSync AND Sync_Required__c = true'
);
}
global void execute(Database.BatchableContext bc, List<Order__c> scope) {
// Prepare payload
List<Map<String,Object>> payload = new List<Map<String,Object>>();
for (Order__c o : scope) {
payload.add(new Map<String,Object>{
'id' => o.Id, 'amount' => o.Amount__c, 'status' => o.Status__c
});
}
// Single callout per batch chunk (up to 200 records)
HttpRequest req = new HttpRequest();
req.setEndpoint('callout:ERP_API/orders/bulk');
req.setMethod('POST');
req.setBody(JSON.serialize(payload));
req.setTimeout(60000);
HttpResponse res = new Http().send(req);
// Mark records as synced
if (res.getStatusCode() == 200) {
List<Order__c> toUpdate = new List<Order__c>();
for (Order__c o : scope) {
toUpdate.add(new Order__c(Id = o.Id, Sync_Required__c = false,
Last_Synced__c = DateTime.now()));
}
update toUpdate;
}
}
global void finish(Database.BatchableContext bc) {
updateLastSyncTimestamp(DateTime.now());
}
}Schedule it with a ScheduledApex class that re-enqueues itself or use Setup > Scheduled Jobs for simpler hourly/nightly runs.
Pattern 4: UI Update (Streaming to the Browser)
When a user needs to see real-time updates without refreshing the page β a live order status tracker, an IoT sensor dashboard, a chatbot interface β the UI Update pattern uses Salesforceβs Streaming API via Lightning Web Components.
// orderTracker.js
import { LightningElement, wire } from 'lwc';
import { subscribe, MessageContext } from 'lightning/empApi';
export default class OrderTracker extends LightningElement {
channelName = '/event/Order_Status_Update__e';
subscription = {};
connectedCallback() {
this.subscribeToChannel();
}
subscribeToChannel() {
const messageCallback = (response) => {
const event = response.data.payload;
this.updateOrderStatus(event.Order_Id__c, event.New_Status__c);
};
subscribe(this.channelName, -1, messageCallback).then(response => {
this.subscription = response;
});
}
updateOrderStatus(orderId, newStatus) {
const statusBadge = this.template.querySelector(`[data-order-id="${orderId}"]`);
if (statusBadge) {
statusBadge.textContent = newStatus;
statusBadge.className = `status-badge status-${newStatus.toLowerCase()}`;
}
}
}Decision Matrix
| Factor | Request-Reply | Fire-and-Forget | Batch Sync | UI Update |
|---|---|---|---|---|
| Needs synchronous response | Yes | No | No | No |
| Handles failures gracefully | Manual retry | Replay buffer | Retry logic | Re-subscribe |
| Volume supported | Low (per callout) | Medium (250k/day) | Very High (millions) | Low (active sessions) |
| User waits for result | Yes | No | No | Pushed |
| Latency tolerance | Seconds | Minutes | Hours | Real-time |
| Best for | Validations, lookups | Notifications, decoupled updates | ETL, data migration | Live dashboards |
The Problem
Scenario: A team builds an order confirmation integration using synchronous callouts. The ERP system goes down for a scheduled maintenance window. During that 30-minute window, every order save in Salesforce throws a callout exception, the transaction rolls back, and sales reps cannot save orders at all β the ERP outage becomes a Salesforce outage.
The Solution
Redesign using the Fire-and-Forget pattern with Platform Events. The order save publishes an Order_Confirmed__e event, which succeeds immediately regardless of ERP availability. A Platform Event trigger processes the event and calls the ERP. If the ERP is down, the event replays automatically for up to 72 hours β no data is lost and order saving is never blocked.
The Mistake I See Most Often
Always set explicit callout timeouts shorter than the 120-second Salesforce maximum. A 10-15 second timeout on synchronous callouts prevents a slow external API from freezing the userβs browser. If your business process truly requires waiting more than 15 seconds for an external response, reconsider whether synchronous is the right pattern.
Teams default to Request-Reply because it is the most familiar pattern β it feels like calling a function. They use it for everything, including operations that do not need a synchronous response, then struggle when the external system has downtime.
Platform Events are underused relative to how powerful they are. The 72-hour event replay capability alone makes them worth using for any integration where you cannot afford to lose events during system maintenance windows.
If you find yourself writing retry logic for a synchronous callout, it is probably the wrong pattern. That is a signal the operation should be asynchronous.
Which integration pattern do you reach for most in your projects, and what made you choose it over the alternatives? I am curious whether the decision is usually architecture-driven or driven by what the team already knows.
Knowledge Check
How did this article make you feel?
Comments
Salesforce Tip