Most integration problems in Salesforce are not coding problems. They are pattern-selection problems that surfaced eighteen months later, under production volume, on the day a downstream service had an outage.
The decision is usually made implicitly — someone needs data in another system, writes a callout in a trigger, and the pattern is chosen by accident. Here is how to choose it deliberately.
Start with four questions
- 01Does the user need the result before their save completes? If not, nothing should be synchronous.
- 02What happens if the downstream system is unavailable for an hour? If the answer is 'we lose data', you need a queue.
- 03What volume will this carry at peak, not at demo?
- 04Who owns the failure when it happens, and how do they find out?
The fourth question eliminates more bad designs than the other three combined. An integration with no owner and no alerting is an outage you will discover from a customer.
Request and reply
Salesforce calls out and waits for a response, which is used immediately. Appropriate when the user genuinely cannot proceed without the answer — a credit check during application, an address validation, a real-time price lookup.
- Never place this in a trigger. Callouts in the save path couple your user experience to someone else's uptime.
- Use a screen flow or an LWC calling imperative Apex, so the wait is visible and cancellable.
- Set an explicit timeout and design the UI for the timeout case, because it will happen.
Fire and forget
Salesforce sends data and does not wait. This is the most common pattern and the most commonly broken one, because 'do not wait' is frequently implemented as 'do not check'.
Done properly, fire-and-forget means publishing a platform event and letting a subscriber handle delivery, retry and failure logging. Done improperly, it means an @future method with an empty catch block.
// Publish from a single trigger handler — the callout never blocks the save.
List<Order_Sync__e> events = new List<Order_Sync__e>();
for (Order o : changedOrders) {
events.add(new Order_Sync__e(
Order_Id__c = o.Id,
Idempotency_Key__c = o.Id + '-' + String.valueOf(o.SystemModstamp.getTime())
));
}
List<Database.SaveResult> results = EventBus.publish(events);
IntegrationLogger.logPublishFailures(results);The idempotency key matters. Retries are inevitable, and without a key the downstream system cannot tell a retry from a genuine second event.
Batch data synchronisation
Scheduled, bulk movement of records in either direction. Right for high-volume, latency-tolerant data — nightly product catalogue refreshes, financial postings, historical loads.
- Use Bulk API 2.0 for inbound volume rather than row-by-row REST calls.
- Track a high-water mark so each run only processes what changed since the last successful run.
- Make the job resumable. A batch that must be rerun from the beginning after a mid-run failure will eventually exceed its window.
- Emit a reconciliation summary on every run — counts in, counts out, counts failed.
Remote call-in
An external system calls Salesforce. Use standard REST or SOAP APIs where they fit, and custom Apex REST services where you need to encapsulate business logic or shield callers from your schema.
- Authenticate with a Connected App and OAuth 2.0 client credentials or JWT bearer flow — never a shared username and password.
- Give the integration user a dedicated profile with least-privilege access.
- Bulkify your custom endpoints. Callers will batch requests whether or not you designed for it.
- Version the endpoint from day one. Changing an unversioned endpoint breaks every caller simultaneously.
Event-driven
Platform events and Change Data Capture decouple producers from consumers entirely. This is usually the right answer when more than two systems care about the same change, or when you want the publisher to be unaware of who is listening.
The trade-off is delivery semantics. Platform events are at-least-once, not exactly-once, and the event bus has a retention window. Consumers must be idempotent and must handle replay after downtime. Design for both before you go live, not after your first outage.
The non-negotiables, whichever pattern you pick
- An integration log object with the payload reference, attempt count, status and error — queryable by someone who is not a developer.
- A dead-letter destination for messages that exhaust their retry budget, so nothing disappears.
- Alerting routed to a person, not to a debug log nobody reads.
- A reconciliation job that periodically proves both systems still agree.
- Documented API limit consumption, modelled at peak volume rather than average.
Pick the pattern deliberately, write down why, and build the observability at the same time as the happy path. The integrations that survive are the ones that fail loudly.