ESC

AI-powered search across all blog posts and tools

Integration Β· March 12, 2026

Salesforce Integration Patterns - When to Use What

A practical guide to the four core integration patterns and exactly when each one is the right tool for the job.

☕ 10 min read 📅 March 12, 2026
  • Request-Reply is the right pattern when you need a synchronous response to continue your business logic
  • Platform Events enable reliable Fire-and-Forget with at-least-once delivery guarantees and replay capability
  • Never choose an integration pattern based on familiarity β€” choose based on latency tolerance and failure requirements

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

Integration Patterns Architecture Overview
Integration Patterns Architecture1. REQUEST-REPLY (Synchronous)SalesforcerequestExternal APIresponse (sync)Use when: Need data back immediatelyRisk: Timeout if external system slowMax callout time: 120 seconds2. FIRE-AND-FORGET (Async / Platform Events)SalesforceevtSubscriber ASubscriber BUse when: Don’t need immediate responseBenefit: Decoupled, replay up to 72 hoursThroughput: 250k events/day (free)3. BATCH DATA SYNCData SourceBatchProcessorSalesforceUse when: Large volumes, latency acceptableBenefit: Handles millions of recordsTypical schedule: Nightly or hourly4. UI UPDATE (Streaming / Pushed)ExternalLWC / UIPlatform Event pushUse when: Real-time UI refresh neededTech: empApi in LWC, Streaming APIExample: Live order status, IoT dashboardsPattern selection drives architecture β€” choose before writing any code

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());
        }
    }
}
⚠️ Critical Considerations
  • 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

FactorRequest-ReplyFire-and-ForgetBatch SyncUI Update
Needs synchronous responseYesNoNoNo
Handles failures gracefullyManual retryReplay bufferRetry logicRe-subscribe
Volume supportedLow (per callout)Medium (250k/day)Very High (millions)Low (active sessions)
User waits for resultYesNoNoPushed
Latency toleranceSecondsMinutesHoursReal-time
Best forValidations, lookupsNotifications, decoupled updatesETL, data migrationLive 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

πŸ’‘ Pro Tip

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.

ℹ️ Rule of Thumb

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

An ERP system has regular 30-minute maintenance windows. Your Salesforce integration sends order confirmations. Which pattern prevents ERP downtime from blocking Salesforce users?
You need to validate an address with an external API before saving a Contact. Which integration pattern is appropriate?

How did this article make you feel?

Comments

Salesforce Tip

🎉

You finished this article!

What to read next

Contents