Salesforce Interview Questions & Answers Book is now available!

Grab Now

Salesforce integration

Salesforce Integration Patterns Explained (With When to Use Each)

The six Salesforce integration patterns, the tools behind each (callouts, Platform Events, CDC, Bulk API 2.0, Apex REST, Connect) and a table for choosing.

By Teja, ex-Salesforce engineer · Updated October 6, 2026 · 10 min read

TL;DR

  • There are six core patterns: request-reply, fire-and-forget, batch data sync, remote call-in, UI update based on data changes, and data virtualization.
  • Pick by answering three questions: who starts the conversation, does the caller need an answer right now, and how much data moves at once.
  • Every integration fails eventually. Design for timeouts, retries and idempotency from the start, not after the first incident.

A Salesforce integration pattern is a proven answer to "how should these two systems talk?" Salesforce's own integration patterns guide names six: request-reply, fire-and-forget, batch data synchronization, remote call-in, UI update based on data changes and data virtualization. Each one maps to specific tools. Request-reply uses Apex callouts with Named Credentials, fire-and-forget uses Platform Events, sync uses Bulk API 2.0 and Change Data Capture, call-in uses the REST APIs or Apex REST, UI updates use streaming events, and virtualization uses Salesforce Connect.

This post explains each pattern, when to choose it, how it fails and how to recover, with a decision table and two pieces of code you can deploy.

The three questions that pick the pattern

Before you choose a tool, answer three questions:

  1. Who starts it? Salesforce calling out, or an external system calling in?
  2. Does the caller need an answer now? Is a user staring at a spinner, or can the work happen later?
  3. How much data, how often? One record when a user clicks a button, or two million rows every night?

Almost every bad integration I've debugged got one of these wrong. Usually it was a synchronous callout doing a job that should have been asynchronous, or row-by-row API calls doing a job that should have been bulk.

Pattern 1: Request-reply (remote process invocation)

What it is: Salesforce calls an external system and waits for the response before continuing. Typical examples are a credit check before an Opportunity moves stage, a live shipping quote, or address validation on save.

Salesforce tools: Apex callouts (HttpRequest / Http) authenticated through Named Credentials, External Services for OpenAPI-described APIs that admins can call from Flow, and HTTP Callout actions in Flow.

When to use it: The user or process can't move forward without the answer, and the external system reliably responds in a few seconds.

Watch out for:

  • Limits. Each transaction can make up to 100 callouts. The default timeout is 10 seconds and the maximum is 120 seconds, which is also the cap on total callout time per transaction.
  • No callouts after uncommitted DML. If you insert a record and then call out in the same transaction, you get "You have uncommitted work pending". Call out first, or move the callout to async Apex.
  • No synchronous callouts from triggers. A trigger has to hand the work to a Queueable or a @future(callout=true) method, which means it isn't request-reply any more.

Named Credentials are not optional in my book. With the current model, an External Credential holds the authentication protocol and secrets, a Named Credential holds the URL, and a permission set grants users access to the credential's principal. Your code references callout:My_Named_Credential/path and never sees a password or token.

Pattern 2: Fire-and-forget (remote process invocation)

What it is: Salesforce tells another system that something happened and doesn't wait for it to finish. For example, an Opportunity is Closed Won and the ERP needs to create an order. The user doesn't need the ERP's order number before moving on.

Salesforce tools:

  • Platform Events. Publish from Apex (EventBus.publish), Flow or the API. External systems subscribe through the Pub/Sub API, and Apex triggers or Flows can subscribe inside Salesforce.
  • Asynchronous Apex callouts. A Queueable that implements Database.AllowsCallouts pushes the data directly when there's exactly one known receiver.
  • Outbound messages still exist in older orgs and come with built-in retries, but I wouldn't start a new design with them.

When to use it: The caller doesn't need the result to continue, and you'd rather the integration fail separately than block a user's save.

Platform Events or a Queueable callout? If there's one receiver and you control the retries, a Queueable is simpler. If several systems care about the same business event, or the receiver should pull at its own pace, publish an event. Set the event's publish behaviour to Publish After Commit for business events, so subscribers never hear about a record that was rolled back.

Subscribers can replay missed events by replay ID. High-volume platform events stay on the event bus for a 72-hour retention window, and that window is what saves you when a subscriber has been down overnight.

Pattern 3: Batch data synchronization

What it is: Large volumes of data are kept in step between Salesforce and another system on a schedule or as a stream. Typical examples are a nightly product catalogue load from the ERP, or replicating Accounts and Contacts to a data warehouse.

Salesforce tools:

  • Bulk API 2.0 for loading or extracting large volumes. You create a job, upload CSV data, and Salesforce handles the batching. It's the right tool for ETL and middleware loading thousands to millions of records.
  • Change Data Capture (CDC) for streaming changes out. Enable it per object, and Salesforce publishes a change event (for example, AccountChangeEvent) for every create, update, delete and undelete, with no code. A warehouse subscribed through the Pub/Sub API stays current without polling.
  • Batch Apex with callouts for modest volumes when Salesforce has to drive the sync itself.

When to use it: Data volume is high and slight latency is acceptable. If you find yourself writing a loop that calls the REST API once per record for 50,000 records, this is the pattern you actually wanted.

Watch out for: Upsert on an external ID field, never on Salesforce IDs from another org or on names. External IDs make loads repeatable and idempotent, so re-running a failed job doesn't create duplicates. Also check which triggers and Flows fire on the target object. A bulk load runs your automation for every record, so automation that isn't bulkified will fail on the first big load.

Pattern 4: Remote call-in

What it is: An external system calls into Salesforce to create, read, update or delete data. Examples include a website creating Leads, a mobile app reading Cases, or a billing system updating payment status.

Salesforce tools:

  • Standard REST API, Composite API and sObject Collections for create, read, update and delete with no code. Composite lets a client chain several requests in one round trip.
  • Apex REST (@RestResource) when the caller needs custom logic: validation, several objects changed in one transaction, or a response shaped for that client.
  • Bulk API 2.0 when the caller sends volume.

Authentication: The external system authenticates with OAuth 2.0 through a connected app or its newer equivalent, an external client app. For server-to-server, use the JWT bearer flow or the client credentials flow, and run it as a dedicated API-only integration user. Salesforce provides a Salesforce Integration user licence for exactly this. Give that user a permission set with only the objects and fields it needs.

When to use Apex REST: Only when the standard APIs would force the client to make several calls and handle partial failures itself. Every custom endpoint is code you now own, test and version.

Pattern 5: UI update based on data changes

What it is: A user's screen updates when data changes somewhere else, without anyone pressing refresh. For example, a support agent sees a Case's status flip the moment the field service system closes the job.

Salesforce tools: Platform Events or CDC as the signal, and the lightning/empApi module in a Lightning web component to subscribe in Lightning Experience. When the event arrives, the component calls notifyRecordUpdateAvailable from lightning/uiRecordApi (or refreshApex for Apex-wired data) to pull fresh values.

When to use it: Users act on data that changes outside their own session, and stale data causes real mistakes, such as two agents working the same Case.

Watch out for: Each subscribed component holds a streaming connection. Subscribe to the narrowest channel that works (/data/CaseChangeEvent rather than every change), filter in the component, and unsubscribe in disconnectedCallback.

Pattern 6: Data virtualization

What it is: Salesforce shows external data without copying it. For example, order history from the ERP appears as a related list on the Account, but the rows stay in the ERP.

Salesforce tools: Salesforce Connect, which exposes external tables as external objects (API names end in __x). Adapters include OData 2.0 and 4.0, cross-org for another Salesforce org, and a custom adapter written with the Apex Connector Framework.

When to use it: The data is large, changes often in the source system, and users mostly need to see it rather than report on it heavily or automate on it. Not copying it means there's no sync job to break and no storage to pay for.

Watch out for: Every page view becomes a call to the external system, so its latency and availability become yours. External objects also don't support everything a custom object does, so check the reporting, search and automation features you need before you commit. Salesforce Connect is licensed separately in most editions, so confirm that before you design around it.

Decision table: which pattern should you use?

ScenarioPatternPrimary tools
User needs an external answer to continue (credit check, quote)Request-replyApex callout + Named Credential, External Services, Flow HTTP Callout
Tell one system something happened; no answer neededFire-and-forgetQueueable callout with Database.AllowsCallouts
Tell several systems something happenedFire-and-forgetPlatform Events, subscribers on the Pub/Sub API
Load or extract large volumes on a scheduleBatch data syncBulk API 2.0 with external IDs
Keep a warehouse or another system current with every changeBatch data sync (streaming)Change Data Capture + Pub/Sub API
External system creates or updates Salesforce recordsRemote call-inREST API, Composite API, sObject Collections
External system needs custom logic in one callRemote call-inApex REST (@RestResource)
Screen must refresh when data changes elsewhereUI updatePlatform Events or CDC + lightning/empApi
Show external data without storing itData virtualizationSalesforce Connect, external objects

Failure handling and retries

Every integration fails eventually. The network times out, the other system deploys at the wrong moment, or a token expires. These are the rules I design to from day one:

  1. Classify the failure. Timeouts, connection errors, HTTP 429 and 5xx are usually transient, so retry them. HTTP 400, 401, 403 and 404 are usually permanent, so retrying just repeats the failure. Log them and alert a human.
  2. Make the receiver idempotent. A retry must never create a second order. Send a stable key (the Salesforce record ID or an external ID) and have the receiver upsert on it.
  3. Back off and cap attempts. Retry with a growing delay and stop after a few attempts. An unbounded retry loop against a struggling system makes the outage worse.
  4. Leave a trail. When the last retry fails, record it somewhere a person will see it, with the payload and the error. A silent failure is the expensive kind.
  5. Use the platform's retries where they exist. A platform event Apex trigger can throw EventBus.RetryableException to have the batch redelivered. Pub/Sub API subscribers can resume from the last replay ID they processed.

Sample code: a fire-and-forget callout with retries

This Queueable sends Closed Won Opportunities to an ERP through a Named Credential called ERP_API. It sends the whole set in one callout, retries transient failures with a growing delay, and creates a task for the owner when it finally gives up. The ERP upserts on salesforceId, so retries are safe.

public with sharing class OpportunityErpSyncJob implements Queueable, Database.AllowsCallouts {
    private static final Integer MAX_ATTEMPTS = 3;

    private final Set<Id> opportunityIds;
    private final Integer attempt;

    public OpportunityErpSyncJob(Set<Id> opportunityIds, Integer attempt) {
        this.opportunityIds = opportunityIds;
        this.attempt = attempt;
    }

    public void execute(QueueableContext context) {
        List<Opportunity> opportunities = [
            SELECT Id, Name, Amount, CloseDate, OwnerId
            FROM Opportunity
            WHERE Id IN :opportunityIds
            WITH USER_MODE
        ];
        if (opportunities.isEmpty()) {
            return;
        }

        Integer status = send(opportunities);
        if (status >= 200 && status < 300) {
            return;
        }

        if (isRetryable(status) && attempt < MAX_ATTEMPTS) {
            // Delay is in minutes (0-10): 2, then 4.
            System.enqueueJob(new OpportunityErpSyncJob(opportunityIds, attempt + 1), attempt * 2);
            return;
        }

        List<Task> alerts = new List<Task>();
        for (Opportunity opp : opportunities) {
            alerts.add(new Task(
                Subject = 'ERP sync failed (status ' + status + ')',
                WhatId = opp.Id,
                OwnerId = opp.OwnerId,
                Priority = 'High'
            ));
        }
        insert as user alerts;
    }

    private Integer send(List<Opportunity> opportunities) {
        List<Map<String, Object>> payload = new List<Map<String, Object>>();
        for (Opportunity opp : opportunities) {
            payload.add(new Map<String, Object>{
                'salesforceId' => opp.Id,
                'name' => opp.Name,
                'amount' => opp.Amount,
                'closeDate' => opp.CloseDate
            });
        }

        HttpRequest req = new HttpRequest();
        req.setEndpoint('callout:ERP_API/v1/orders/bulk-upsert');
        req.setMethod('POST');
        req.setHeader('Content-Type', 'application/json');
        req.setTimeout(30000);
        req.setBody(JSON.serialize(payload));

        try {
            return new Http().send(req).getStatusCode();
        } catch (CalloutException e) {
            return 0; // Timeout or connection failure: treat as transient.
        }
    }

    private static Boolean isRetryable(Integer status) {
        return status == 0 || status == 429 || status >= 500;
    }
}

Start it with System.enqueueJob(new OpportunityErpSyncJob(closedWonIds, 1)); from your trigger handler, once per transaction with all the IDs, not once per record. A transaction can enqueue at most 50 jobs. Keep the payload size sensible too: if a single run could cover thousands of records, use Batch Apex or Bulk API 2.0 instead.

Sample code: an Apex REST resource for remote call-in

This resource lets a customer portal look up a Case by number and create new Cases. It runs with sharing and in user mode, so the integration user's permissions decide what the caller can touch. It returns proper status codes: 201 on create, 400 for bad input, 404 when nothing matches and 422 when a validation rule rejects the record.

@RestResource(urlMapping='/support/v1/cases/*')
global with sharing class SupportCaseResource {

    public class CaseInput {
        public String subject;
        public String description;
    }

    @HttpGet
    global static void getCase() {
        String caseNumber = RestContext.request.requestURI.substringAfterLast('/');
        List<Case> cases = [
            SELECT CaseNumber, Subject, Status, Priority
            FROM Case
            WHERE CaseNumber = :caseNumber
            WITH USER_MODE
            LIMIT 1
        ];

        if (cases.isEmpty()) {
            respond(404, new Map<String, Object>{ 'error' => 'Case not found' });
            return;
        }
        Case found = cases[0];
        respond(200, new Map<String, Object>{
            'caseNumber' => found.CaseNumber,
            'subject' => found.Subject,
            'status' => found.Status,
            'priority' => found.Priority
        });
    }

    @HttpPost
    global static void createCase() {
        CaseInput input;
        try {
            input = (CaseInput) JSON.deserialize(RestContext.request.requestBody.toString(), CaseInput.class);
        } catch (JSONException e) {
            respond(400, new Map<String, Object>{ 'error' => 'Request body is not valid JSON' });
            return;
        }
        if (input == null || String.isBlank(input.subject)) {
            respond(400, new Map<String, Object>{ 'error' => 'subject is required' });
            return;
        }

        Case newCase = new Case(Subject = input.subject, Description = input.description, Origin = 'Web');
        try {
            insert as user newCase;
        } catch (DmlException e) {
            respond(422, new Map<String, Object>{ 'error' => e.getDmlMessage(0) });
            return;
        }
        respond(201, new Map<String, Object>{ 'id' => newCase.Id });
    }

    private static void respond(Integer statusCode, Map<String, Object> body) {
        RestResponse res = RestContext.response;
        res.statusCode = statusCode;
        res.addHeader('Content-Type', 'application/json');
        res.responseBody = Blob.valueOf(JSON.serialize(body));
    }
}

The endpoint is /services/apexrest/support/v1/cases/, and a GET adds the case number to the end. I put a version (v1) in the URL mapping from day one, so a breaking change can ship as v2 without breaking existing clients. Test it with RestContext.request and RestContext.response set to new instances in an Apex test, which my Apex test class guide covers alongside callout mocks.

Putting the patterns together

Real integrations combine patterns. A typical order-to-cash setup I'd design looks like this:

  • Fire-and-forget: Closed Won publishes an event or enqueues a callout, and the ERP creates the order.
  • Remote call-in: The ERP calls back through the REST API to write the order number and status onto the Opportunity.
  • UI update: The sales rep's record page refreshes when that status changes.
  • Data virtualization: Invoice history appears from the ERP through Salesforce Connect, without being copied.
  • Batch sync: Products and price books load every night through Bulk API 2.0.

Each piece fails on its own, retries on its own, and can be explained in one sentence. That's the sign of a design you'll still understand a year later.

Learning integration properly

The Salesforce integration guide lays out the topics in order, the integration specialist roadmap shows where they fit in a career, and the integration interview questions test whether you can explain these trade-offs out loud.

My Salesforce Integration Master Course (Advanced Apex) is the largest course on Salesforce Casts, at 27 hours. It covers OAuth flows, External and Named Credentials, connected apps, single sign-on, REST APIs in Apex, Platform Events, Bulk API 2.0, the Streaming API, JWT and more. Every integration is built live against real endpoints, including the failures, because that's where integration skills come from.

Salesforce Integration Master Course (Advanced Apex)

151 episodes · 27 hours · 26 modules

View syllabus →

Learn it by building it

Every course, every code repository, the discussion group and certificates of completion — one subscription.

Frequently asked questions

What are the main Salesforce integration patterns?

Salesforce's own integration patterns guide describes request-reply, fire-and-forget, batch data synchronization, remote call-in, UI update based on data changes and data virtualization. Most real integrations combine two or three of them.

When should I use Platform Events instead of an Apex callout?

Use an Apex callout when Salesforce needs the external system's answer to continue. Use Platform Events when Salesforce only needs to announce that something happened and one or more subscribers can process it on their own schedule.

What's the difference between Platform Events and Change Data Capture?

Platform Events are custom messages you define and publish deliberately. Change Data Capture publishes an event automatically whenever a record on an enabled object is created, updated, deleted or undeleted, with no publishing code.

Should I build an Apex REST service or use the standard REST API?

Use the standard REST API, Composite API or sObject Collections for plain create, read, update and delete. Build an Apex REST resource only when the caller needs custom logic, validation or several objects changed in one transaction behind a single endpoint.

How do I retry a failed callout in Salesforce?

Run the callout asynchronously, treat timeouts, 429 and 5xx responses as retryable, and re-enqueue the Queueable with a delay and an attempt counter. Make the receiving system idempotent so a retry can never create a duplicate.

Keep going

Keep me in the loop!

Email me when a new episode is published (promise, I will not spam you)

You're signing up to receive emails from Salesforce Casts