Introduction
Integrating Salesforce with external systems is a common requirement for businesses looking to create a unified digital ecosystem. A well-designed integration architecture ensures seamless data flow, enhanced business processes, and a better overall user experience.
Understanding Integration Design Patterns
Integration design patterns are proven solutions to recurring integration challenges. They provide a structured approach to connecting Salesforce with external applications, databases, and services. Salesforce identifies several key integration patterns:
- Remote Process Invocation—Request and Reply
- Remote Process Invocation—Fire and Forget
- Batch Data Synchronization
- Remote Call-In
- UI Update Based on Data Changes
Let’s explore each pattern in detail.
1. Remote Process Invocation—Request and Reply
Overview
This pattern involves Salesforce making a request to an external system and waiting for a response before proceeding. It’s ideal for scenarios requiring immediate feedback or validation.
When to Use
- Real-time address validation
- Credit checks
- Inventory availability checks
- Price calculations

Implementation Example
Here’s how to implement this pattern using Apex and the HTTP protocol:
public class ExternalSystemCallout {
// Method to perform callout to external system
public static ExternalSystemResponse makeCallout(String requestData) {
// Prepare HTTP request
HttpRequest req = new HttpRequest();
req.setEndpoint('https://api.externalsystem.com/service');
req.setMethod('POST');
req.setHeader('Content-Type', 'application/json');
req.setBody(requestData);
req.setTimeout(120000); // Set timeout to 120 seconds
// Execute the callout
Http http = new Http();
HttpResponse res;
try {
res = http.send(req);
// Process the response
if (res.getStatusCode() == 200) {
// Parse response and create response object
return parseResponse(res.getBody());
} else {
throw new ExternalSystemException('Error from external system: ' +
res.getStatusCode() + ' ' + res.getStatus());
}
} catch (Exception e) {
throw new ExternalSystemException('Callout error: ' + e.getMessage());
}
}
// Parse the JSON response
private static ExternalSystemResponse parseResponse(String responseBody) {
return (ExternalSystemResponse)JSON.deserialize(responseBody, ExternalSystemResponse.class);
}
// Custom exception class
public class ExternalSystemException extends Exception {}
// Response wrapper class
public class ExternalSystemResponse {
public Boolean success;
public String message;
public Map<String, Object> data;
}
}
Apex Trigger Example
trigger AccountAddressValidation on Account (before insert, before update) {
for (Account acc : Trigger.new) {
// Only perform validation if address fields have changed
if (Trigger.isUpdate) {
Account oldAcc = Trigger.oldMap.get(acc.Id);
if (acc.BillingStreet == oldAcc.BillingStreet &&
acc.BillingCity == oldAcc.BillingCity &&
acc.BillingState == oldAcc.BillingState &&
acc.BillingPostalCode == oldAcc.BillingPostalCode &&
acc.BillingCountry == oldAcc.BillingCountry) {
continue; // Skip if address hasn't changed
}
}
// Prepare request data
String requestData = JSON.serialize(new Map<String, String>{
'street' => acc.BillingStreet,
'city' => acc.BillingCity,
'state' => acc.BillingState,
'postalCode' => acc.BillingPostalCode,
'country' => acc.BillingCountry
});
// Make callout to address validation service
try {
ExternalSystemCallout.ExternalSystemResponse response =
ExternalSystemCallout.makeCallout(requestData);
if (!response.success) {
acc.addError('Address validation failed: ' + response.message);
} else if (response.data != null) {
// Update with standardized address if available
if (response.data.containsKey('standardizedStreet'))
acc.BillingStreet = (String)response.data.get('standardizedStreet');
if (response.data.containsKey('standardizedCity'))
acc.BillingCity = (String)response.data.get('standardizedCity');
// ... etc for other fields
}
} catch (ExternalSystemCallout.ExternalSystemException e) {
acc.addError('Unable to validate address: ' + e.getMessage());
}
}
}
Best Practices
- Handle timeouts and errors gracefully – Always implement proper error handling.
- Use named credentials – For secure authentication with external systems.
- Consider governor limits – Be mindful of Apex callout limits (100 callouts per transaction).
- Implement bulkification – Design your code to handle bulk operations.
- Use @future for triggers – Use @future methods for callouts from triggers.
2. Remote Process Invocation—Fire and Forget
Overview
In this pattern, Salesforce sends a message to an external system but doesn’t wait for a response. This is useful for asynchronous processing where immediate feedback isn’t required.
When to Use
- Sending emails or notifications
- Updating external systems asynchronously
- Logging events that don’t require immediate confirmation
- Starting long-running processes

Implementation Example
Here’s how to implement this pattern using Platform Events:
// Platform Event Definition
// Create a custom platform event in your org named 'External_System_Update__e'
// with fields 'Record_Id__c', 'Operation_Type__c', 'Payload__c', etc.
// Publishing Event
public class FireAndForgetPublisher {
@future(callout=true)
public static void sendAsyncUpdate(String recordId, String operationType, String payload) {
// Create a Platform Event instance
External_System_Update__e event = new External_System_Update__e(
Record_Id__c = recordId,
Operation_Type__c = operationType,
Payload__c = payload
);
// Publish the event
Database.SaveResult result = EventBus.publish(event);
// Optional: Log errors if publish fails
if (!result.isSuccess()) {
for (Database.Error error : result.getErrors()) {
System.debug('Error publishing event: ' + error.getMessage());
// Consider inserting into an error log object
}
}
}
}
// Trigger to initiate the fire and forget process
trigger OpportunityFireAndForget on Opportunity (after insert, after update) {
List<String> recordIds = new List<String>();
for (Opportunity opp : Trigger.new) {
if (opp.Amount > 100000) { // Only send large opportunities
recordIds.add(opp.Id);
}
}
if (!recordIds.isEmpty()) {
for (String recordId : recordIds) {
// Prepare payload
String payload = JSON.serialize(
new Map<String, Object>{
'opportunityId' => recordId,
'timestamp' => System.now().getTime()
}
);
// Send the async update
FireAndForgetPublisher.sendAsyncUpdate(
recordId,
Trigger.isInsert ? 'INSERT' : 'UPDATE',
payload
);
}
}
}
Event-Driven Architecture with Platform Events
// Platform Event Trigger to process events
trigger ExternalSystemUpdateEvent on External_System_Update__e (after insert) {
// Process batch of events
for (External_System_Update__e event : Trigger.new) {
// Call external system
HttpRequest req = new HttpRequest();
req.setEndpoint('https://api.externalsystem.com/service');
req.setMethod('POST');
req.setHeader('Content-Type', 'application/json');
// Create payload from event data
Map<String, Object> payload = new Map<String, Object>{
'recordId' => event.Record_Id__c,
'operationType' => event.Operation_Type__c,
'data' => event.Payload__c,
'sourceSystem' => 'Salesforce',
'timestamp' => System.now().getTime()
};
req.setBody(JSON.serialize(payload));
// Send asynchronously
try {
Http http = new Http();
HttpResponse res = http.send(req);
// Optional: Log response for monitoring
System.debug('Response: ' + res.getStatusCode() + ' ' + res.getStatus());
} catch (Exception e) {
// Log error but don't retry - this is fire and forget
System.debug('Error sending to external system: ' + e.getMessage());
// Consider inserting into an error log object for monitoring
}
}
}
Best Practices
- Design for failure – Implement error logging and monitoring.
- Use platform events – They provide a scalable publish-subscribe model.
- Consider retry logic – For critical operations, implement a retry mechanism.
- Batch similar requests – Group related operations to reduce API calls.
- Monitor external system health – Implement health checks for dependent systems.
3. Batch Data Synchronization
Overview
This pattern involves scheduled processing of data in bulk between Salesforce and external systems. It’s ideal for scenarios where real-time synchronization isn’t necessary.
When to Use
- Daily/nightly data synchronization
- Mass imports/exports
- Historical data loading
- Periodic reporting

Implementation Example
Here’s an implementation using Batch Apex and Scheduled Apex:
// Batch Apex class for syncing data
public class AccountSyncBatch implements Database.Batchable<sObject>, Database.AllowsCallouts, Schedulable {
// Method to schedule the batch job
public void execute(SchedulableContext sc) {
Database.executeBatch(this, 200); // Process in batches of 200 records
}
// Start method - define the query
public Database.QueryLocator start(Database.BatchableContext bc) {
// Query accounts modified since last sync
// You could store last sync time in a custom setting
DateTime lastSyncTime = getLastSyncTime();
return Database.getQueryLocator(
'SELECT Id, Name, BillingStreet, BillingCity, ' +
'BillingState, BillingPostalCode, BillingCountry, ' +
'Phone, Website, LastModifiedDate ' +
'FROM Account ' +
'WHERE LastModifiedDate > :lastSyncTime'
);
}
// Execute method - process each batch
public void execute(Database.BatchableContext bc, List<Account> accounts) {
// Prepare data for external system
List<Map<String, Object>> recordsToSync = new List<Map<String, Object>>();
for (Account acc : accounts) {
Map<String, Object> record = new Map<String, Object>{
'salesforceId' => acc.Id,
'name' => acc.Name,
'billingStreet' => acc.BillingStreet,
'billingCity' => acc.BillingCity,
'billingState' => acc.BillingState,
'billingPostalCode' => acc.BillingPostalCode,
'billingCountry' => acc.BillingCountry,
'phone' => acc.Phone,
'website' => acc.Website,
'lastModifiedDate' => acc.LastModifiedDate.getTime()
};
recordsToSync.add(record);
}
// Send data to external system
if (!recordsToSync.isEmpty()) {
sendToExternalSystem(recordsToSync);
}
}
// Finish method - update last sync time
public void finish(Database.BatchableContext bc) {
// Update last sync time
updateLastSyncTime(System.now());
// Optional: Send notification that sync is complete
sendCompletionEmail(bc.getJobId());
}
// Method to send data to external system
private void sendToExternalSystem(List<Map<String, Object>> records) {
HttpRequest req = new HttpRequest();
req.setEndpoint('https://api.externalsystem.com/batch-update');
req.setMethod('POST');
req.setHeader('Content-Type', 'application/json');
req.setBody(JSON.serialize(records));
req.setTimeout(120000); // 2 minutes
Http http = new Http();
try {
HttpResponse res = http.send(req);
if (res.getStatusCode() != 200) {
// Log error
System.debug('Error syncing accounts: ' + res.getStatus());
logSyncError(res.getBody());
}
} catch (Exception e) {
// Log exception
System.debug('Exception syncing accounts: ' + e.getMessage());
logSyncError(e.getMessage());
}
}
// Helper methods for managing sync state
private DateTime getLastSyncTime() {
Sync_Settings__c settings = Sync_Settings__c.getInstance('AccountSync');
if (settings != null && settings.Last_Sync_Time__c != null) {
return settings.Last_Sync_Time__c;
}
// Default to 7 days ago if no previous sync
return System.now().addDays(-7);
}
private void updateLastSyncTime(DateTime syncTime) {
Sync_Settings__c settings = Sync_Settings__c.getInstance('AccountSync');
if (settings == null) {
settings = new Sync_Settings__c(Name = 'AccountSync');
}
settings.Last_Sync_Time__c = syncTime;
upsert settings;
}
private void logSyncError(String errorDetails) {
// Insert into custom error log object
Sync_Error_Log__c errorLog = new Sync_Error_Log__c(
Object_Type__c = 'Account',
Error_Details__c = errorDetails,
Error_Time__c = System.now()
);
insert errorLog;
}
private void sendCompletionEmail(String jobId) {
// Send email notification about job completion
Messaging.SingleEmailMessage mail = new Messaging.SingleEmailMessage();
String[] toAddresses = new String[] {'[email protected]'};
mail.setToAddresses(toAddresses);
mail.setSubject('Account Sync Completed - Job ' + jobId);
mail.setPlainTextBody('The account synchronization batch job has completed.');
Messaging.sendEmail(new Messaging.SingleEmailMessage[] { mail });
}
}
// Schedule the batch job
public class ScheduleAccountSync {
public static void scheduleDaily() {
AccountSyncBatch batchJob = new AccountSyncBatch();
// Schedule to run daily at 2 AM
String cronExp = '0 0 2 * * ?';
System.schedule('Daily Account Sync', cronExp, batchJob);
}
}
Handling Inbound Batch Updates from External Systems
@RestResource(urlMapping='/api/v1/batchAccounts/*')
global class ExternalAccountBatchAPI {
@HttpPost
global static BatchResponse updateAccountsFromExternal() {
RestRequest req = RestContext.request;
RestResponse res = RestContext.response;
try {
// Parse the incoming JSON
String requestBody = req.requestBody.toString();
List<Map<String, Object>> accountUpdates =
(List<Map<String, Object>>)JSON.deserializeUntyped(requestBody);
List<Account> accountsToUpdate = new List<Account>();
Map<String, String> externalIdToSfId = new Map<String, String>();
// Process each account record
for (Map<String, Object> accData : accountUpdates) {
String externalId = (String)accData.get('externalId');
String salesforceId = (String)accData.get('salesforceId');
Account acc;
if (salesforceId != null) {
acc = new Account(Id = salesforceId);
} else if (externalId != null) {
acc = new Account(External_ID__c = externalId);
externalIdToSfId.put(externalId, null);
} else {
continue; // Skip records without ID
}
// Map fields
if (accData.containsKey('name')) acc.Name = (String)accData.get('name');
if (accData.containsKey('phone')) acc.Phone = (String)accData.get('phone');
if (accData.containsKey('website')) acc.Website = (String)accData.get('website');
// Map other fields as needed
accountsToUpdate.add(acc);
}
// Update accounts
List<Database.UpsertResult> results = Database.upsert(accountsToUpdate,
Account.External_ID__c, false);
// Process results
List<Map<String, Object>> successRecords = new List<Map<String, Object>>();
List<Map<String, Object>> errorRecords = new List<Map<String, Object>>();
for (Integer i = 0; i < results.size(); i++) {
Database.UpsertResult result = results[i];
Map<String, Object> originalData = accountUpdates[i];
if (result.isSuccess()) {
Map<String, Object> successRecord = new Map<String, Object>{
'externalId' => originalData.get('externalId'),
'salesforceId' => result.getId()
};
successRecords.add(successRecord);
// Update map for external ID to Salesforce ID
String extId = (String)originalData.get('externalId');
if (extId != null) {
externalIdToSfId.put(extId, result.getId());
}
} else {
Map<String, Object> errorRecord = new Map<String, Object>{
'externalId' => originalData.get('externalId'),
'salesforceId' => originalData.get('salesforceId'),
'errors' => parseErrors(result.getErrors())
};
errorRecords.add(errorRecord);
}
}
// Create response
BatchResponse response = new BatchResponse();
response.success = errorRecords.isEmpty();
response.totalProcessed = accountsToUpdate.size();
response.successCount = successRecords.size();
response.errorCount = errorRecords.size();
response.successRecords = successRecords;
response.errorRecords = errorRecords;
response.idMappings = externalIdToSfId;
return response;
} catch (Exception e) {
// Handle any unexpected exceptions
BatchResponse errorResponse = new BatchResponse();
errorResponse.success = false;
errorResponse.message = 'Error processing batch request: ' + e.getMessage();
return errorResponse;
}
}
// Helper method to parse Database.Error objects
private static List<String> parseErrors(List<Database.Error> errors) {
List<String> errorMessages = new List<String>();
for (Database.Error error : errors) {
errorMessages.add(error.getStatusCode() + ': ' + error.getMessage());
}
return errorMessages;
}
// Response class
global class BatchResponse {
global Boolean success;
global String message;
global Integer totalProcessed;
global Integer successCount;
global Integer errorCount;
global List<Map<String, Object>> successRecords;
global List<Map<String, Object>> errorRecords;
global Map<String, String> idMappings;
}
}
Best Practices
- Schedule during off-peak hours – Minimize impact on system performance.
- Implement checkpointing – Store progress in case of failures.
- Use batch processing – Handle large data volumes efficiently.
- Include detailed logging – Track sync operations for troubleshooting.
- Implement data validation – Ensure data quality before syncing.
4. Remote Call-In
Overview
In this pattern, external systems make API calls to Salesforce. Salesforce acts as a service provider exposing functionality through REST, SOAP, or other API protocols.
When to Use
- Providing data to external applications
- Exposing Salesforce business logic to external systems
- Creating a centralized service layer
- Building composite applications

Implementation Example
Here’s how to implement a REST API endpoint in Salesforce:
@RestResource(urlMapping='/api/v1/accounts/*')
global class AccountRestAPI {
@HttpGet
global static Account getAccountById() {
RestRequest req = RestContext.request;
RestResponse res = RestContext.response;
// Get account ID from the URL
String accountId = req.requestURI.substring(
req.requestURI.lastIndexOf('/') + 1);
// Query the account
Account result;
try {
result = [SELECT Id, Name, BillingStreet, BillingCity,
BillingState, BillingPostalCode, BillingCountry,
Phone, Website, Industry, Description
FROM Account WHERE Id = :accountId LIMIT 1];
} catch (Exception e) {
res.statusCode = 404;
return null;
}
// Return the account
return result;
}
@HttpPost
global static AccountResponse createAccount(String name,
String phone,
String website,
String industry) {
// Create new account
Account acc = new Account(
Name = name,
Phone = phone,
Website = website,
Industry = industry
);
// Initialize response
AccountResponse response = new AccountResponse();
try {
// Insert the account
insert acc;
// Set success response
response.success = true;
response.accountId = acc.Id;
response.message = 'Account created successfully';
} catch (DmlException e) {
// Set error response
response.success = false;
response.message = 'Failed to create account: ' + e.getMessage();
RestContext.response.statusCode = 400;
}
return response;
}
@HttpPut
global static AccountResponse updateAccount() {
RestRequest req = RestContext.request;
RestResponse res = RestContext.response;
// Parse the JSON request body
String requestBody = req.requestBody.toString();
Map<String, Object> params = (Map<String, Object>)JSON.deserializeUntyped(requestBody);
// Get account ID from the URL
String accountId = req.requestURI.substring(
req.requestURI.lastIndexOf('/') + 1);
// Initialize response
AccountResponse response = new AccountResponse();
try {
// Query the account first
Account acc = [SELECT Id FROM Account WHERE Id = :accountId LIMIT 1];
// Update fields based on provided parameters
if (params.containsKey('name')) acc.Name = (String)params.get('name');
if (params.containsKey('phone')) acc.Phone = (String)params.get('phone');
if (params.containsKey('website')) acc.Website = (String)params.get('website');
if (params.containsKey('industry')) acc.Industry = (String)params.get('industry');
if (params.containsKey('description')) acc.Description = (String)params.get('description');
// Update billing address if provided
if (params.containsKey('billingStreet')) acc.BillingStreet = (String)params.get('billingStreet');
if (params.containsKey('billingCity')) acc.BillingCity = (String)params.get('billingCity');
if (params.containsKey('billingState')) acc.BillingState = (String)params.get('billingState');
if (params.containsKey('billingPostalCode')) acc.BillingPostalCode = (String)params.get('billingPostalCode');
if (params.containsKey('billingCountry')) acc.BillingCountry = (String)params.get('billingCountry');
// Update the account
update acc;
// Set success response
response.success = true;
response.accountId = acc.Id;
response.message = 'Account updated successfully';
} catch (Exception e) {
// Set error response
response.success = false;
response.message = 'Failed to update account: ' + e.getMessage();
res.statusCode = 400;
}
return response;
}
@HttpDelete
global static AccountResponse deleteAccount() {
RestRequest req = RestContext.request;
RestResponse res = RestContext.response;
// Get account ID from the URL
String accountId = req.requestURI.substring(
req.requestURI.lastIndexOf('/') + 1);
// Initialize response
AccountResponse response = new AccountResponse();
try {
// Query the account
Account acc = [SELECT Id FROM Account WHERE Id = :accountId LIMIT 1];
// Delete the account
delete acc;
// Set success response
response.success = true;
response.accountId = accountId;
response.message = 'Account deleted successfully';
} catch (Exception e) {
// Set error response
response.success = false;
response.message = 'Failed to delete account: ' + e.getMessage();
res.statusCode = e instanceof QueryException ? 404 : 400;
}
return response;
}
// Response wrapper class
global class AccountResponse {
global Boolean success;
global String accountId;
global String message;
}
}
Custom Authentication for API Endpoints
@RestResource(urlMapping='/api/v1/*')
global class APIAuthHandler {
// Custom header for API key
private static final String API_KEY_HEADER = 'X-API-Key';
// Method to validate API key
global static Boolean validateAPIKey() {
RestRequest req = RestContext.request;
RestResponse res = RestContext.response;
// Get API key from header
String apiKey = req.headers.get(API_KEY_HEADER);
if (apiKey == null) {
res.statusCode = 401;
res.addHeader('WWW-Authenticate', 'X-API-Key realm="Salesforce API"');
return false;
}
// Validate API key against custom setting or custom metadata
API_Key__mdt keyRecord;
try {
keyRecord = [SELECT Id, MasterLabel, Active__c
FROM API_Key__mdt
WHERE Key_Value__c = :apiKey LIMIT 1];
} catch (Exception e) {
res.statusCode = 403;
return false;
}
if (keyRecord == null || !keyRecord.Active__c) {
res.statusCode = 403;
return false;
}
return true;
}
// Example middleware implementation
public static void applyAPIAuth() {
if (!validateAPIKey()) {
// Create error response
Map<String, Object> errorResponse = new Map<String, Object>{
'success' => false,
'errorCode' => 'INVALID_API_KEY',
'message' => 'Invalid or missing API key'
};
// Set response body
RestContext.response.responseBody = Blob.valueOf(JSON.serialize(errorResponse));
// This will effectively stop execution of the main API logic
}
}
}
// Example implementation of API with authentication
@RestResource(urlMapping='/api/v1/contacts/*')
global class ContactRestAPI {
@HttpGet
global static List<Contact> getContacts() {
// Apply API authentication
APIAuthHandler.applyAPIAuth();
// If authentication failed, response is already set and this will be skipped
if (RestContext.response.statusCode >= 400) {
return null;
}
// API logic here
return [SELECT Id, FirstName, LastName, Email, Phone
FROM Contact LIMIT 100];
}
}
Best Practices
- Implement proper authentication – Use OAuth, JWT, or API keys.
- Apply rate limiting – Prevent abuse of your API endpoints.
- Handle bulk operations – Design APIs to handle bulk data efficiently.
- Provide comprehensive error messages – Make troubleshooting easier.
- Document your APIs – Use clear documentation for external developers.
5. UI Update Based on Data Changes
Overview
This pattern involves updating the user interface in response to data changes in Salesforce or external systems. It’s essential for delivering a dynamic and responsive user experience.
When to Use
- Real-time dashboards
- Collaborative applications
- Notifications and alerts
- Process monitoring

Implementation Example
Here’s how to implement this pattern using Lightning Web Components:
// contactUpdater.js - LWC JavaScript file
import { LightningElement, wire, api, track } from 'lwc';
import { getRecord, getFieldValue, updateRecord } from 'lightning/uiRecordApi';
import { subscribe, unsubscribe, onError } from 'lightning/empApi';
import { ShowToastEvent } from 'lightning/platformShowToastEvent';
// Import schema
import CONTACT_ID_FIELD from '@salesforce/schema/Contact.Id';
import CONTACT_NAME_FIELD from '@salesforce/schema/Contact.Name';
import CONTACT_EMAIL_FIELD from '@salesforce/schema/Contact.Email';
import CONTACT_PHONE_FIELD from '@salesforce/schema/Contact.Phone';
export default class ContactUpdater extends LightningElement {
@api recordId;
@track contact;
@track error;
// Channel name for Platform Events
channelName = '/event/Contact_Update__e';
subscription = null;
// Fields to retrieve
fields = [CONTACT_ID_FIELD, CONTACT_NAME_FIELD, CONTACT_EMAIL_FIELD, CONTACT_PHONE_FIELD];
// Wire to get contact data
@wire(getRecord, { recordId: '$recordId', fields: '$fields' })
wiredContact({ error, data }) {
if (data) {
this.contact = data;
this.error = undefined;
} else if (error) {
this.error = error;
this.contact = undefined;
}
}
// Connect callback - subscribe to platform events
connectedCallback() {
// Subscribe to platform event channel
this.subscribeToChannel();
// Register error listener
this.registerErrorListener();
}
// Disconnect callback - unsubscribe from platform events
disconnectedCallback() {
this.unsubscribeFromChannel();
}
// Subscribe to the platform event channel
subscribeToChannel() {
const messageCallback = (response) => {
console.log('Event received: ', JSON.stringify(response));
// Extract data from event
const eventData = response.data.payload;
// Check if this event is for our contact
if (eventData.Contact_Id__c === this.recordId) {
// Show toast notification
this.dispatchEvent(
new ShowToastEvent({
title: 'Contact Updated',
message: 'Contact information has been updated in an external system.',
variant: 'info'
})
);
// Refresh the record to show latest data
this.refreshContact();
}
};
// Subscribe to the channel
subscribe(this.channelName, -1, messageCallback)
.then(response => {
this.subscription = response;
console.log('Subscribed to channel:', this.channelName);
})
.catch(error => {
console.error('Error subscribing to channel:', error);
});
}
// Unsubscribe from the platform event channel
unsubscribeFromChannel() {
if (this.subscription) {
unsubscribe(this.subscription)
.then(() => {
console.log('Unsubscribed from channel:', this.channelName);
this.subscription = null;
})
.catch(error => {
console.error('Error unsubscribing from channel:', error);
});
}
}
// Register error listener for platform events
registerErrorListener() {
onError(error => {
console.error('Received error from server:', error);
// Handle error
});
}
// Method to refresh contact data
refreshContact() {
// Use getRecordNotifyChange to refresh the record
getRecordNotifyChange([{recordId: this.recordId}]);
}
// Getter methods for contact fields
get name() {
return getFieldValue(this.contact, CONTACT_NAME_FIELD);
}
get email() {
return getFieldValue(this.contact, CONTACT_EMAIL_FIELD);
}
get phone() {
return getFieldValue(this.contact, CONTACT_PHONE_FIELD);
}
}
// contactUpdater.html - LWC HTML template
```html
<template>
<lightning-card title="Contact Information" icon-name="standard:contact">
<div class="slds-p-around_medium">
<template if:true={contact}>
<div class="slds-grid slds-wrap">
<div class="slds-col slds-size_1-of-1 slds-medium-size_1-of-2 slds-p-bottom_small">
<div class="slds-form-element">
<span class="slds-form-element__label">Name</span>
<div class="slds-form-element__control">
<div class="slds-form-element__static">{name}</div>
</div>
</div>
</div>
<div class="slds-col slds-size_1-of-1 slds-medium-size_1-of-2 slds-p-bottom_small">
<div class="slds-form-element">
<span class="slds-form-element__label">Email</span>
<div class="slds-form-element__control">
<div class="slds-form-element__static">{email}</div>
</div>
</div>
</div>
<div class="slds-col slds-size_1-of-1 slds-medium-size_1-of-2 slds-p-bottom_small">
<div class="slds-form-element">
<span class="slds-form-element__label">Phone</span>
<div class="slds-form-element__control">
<div class="slds-form-element__static">{phone}</div>
</div>
</div>
</div>
</div>
<div class="slds-p-top_medium">
<lightning-button label="Refresh" onclick={refreshContact}
variant="brand"></lightning-button>
</div>
</template>
<template if:true={error}>
<div class="slds-text-color_error">
Error loading contact: {error.body.message}
</div>
</template>
</div>
</lightning-card>
</template>
Platform Event Definition for Contact Updates
<!-- Contact_Update__e Platform Event Meta XML -->
<?xml version="1.0" encoding="UTF-8"?>
<CustomObject xmlns="http://soap.sforce.com/2006/04/metadata">
<deploymentStatus>Deployed</deploymentStatus>
<eventType>StandardVolume</eventType>
<label>Contact Update</label>
<pluralLabel>Contact Updates</pluralLabel>
<publishBehavior>PublishAfterCommit</publishBehavior>
<fields>
<fullName>Contact_Id__c</fullName>
<externalId>false</externalId>
<isFilteringDisabled>false</isFilteringDisabled>
<isNameField>false</isNameField>
<isSortingDisabled>false</isSortingDisabled>
<label>Contact Id</label>
<length>18</length>
<required>true</required>
<type>Text</type>
<unique>false</unique>
</fields>
<fields>
<fullName>External_System_Id__c</fullName>
<externalId>false</externalId>
<isFilteringDisabled>false</isFilteringDisabled>
<isNameField>false</isNameField>
<isSortingDisabled>false</isSortingDisabled>
<label>External System Id</label>
<length>50</length>
<required>false</required>
<type>Text</type>
<unique>false</unique>
</fields>
<fields>
<fullName>Update_Type__c</fullName>
<externalId>false</externalId>
<isFilteringDisabled>false</isFilteringDisabled>
<isNameField>false</isNameField>
<isSortingDisabled>false</isSortingDisabled>
<label>Update Type</label>
<length>20</length>
<required>false</required>
<type>Text</type>
<unique>false</unique>
</fields>
</CustomObject>
Apex Class to Publish Platform Events
public class ContactEventPublisher {
// Method to publish contact update event
public static void publishContactUpdate(String contactId, String externalSystemId, String updateType) {
// Create a new platform event instance
Contact_Update__e event = new Contact_Update__e(
Contact_Id__c = contactId,
External_System_Id__c = externalSystemId,
Update_Type__c = updateType
);
// Publish the event
Database.SaveResult result = EventBus.publish(event);
// Inspect result
if (!result.isSuccess()) {
for (Database.Error error : result.getErrors()) {
System.debug('Error publishing contact update event: ' + error.getMessage());
}
}
}
}
REST API Endpoint to Receive External Updates
@RestResource(urlMapping='/api/v1/contactUpdate/*')
global class ContactUpdateAPI {
@HttpPost
global static UpdateResponse processUpdate() {
RestRequest req = RestContext.request;
RestResponse res = RestContext.response;
try {
// Parse JSON request
String requestBody = req.requestBody.toString();
Map<String, Object> params = (Map<String, Object>)JSON.deserializeUntyped(requestBody);
// Extract data
String externalId = (String)params.get('externalId');
String salesforceId = (String)params.get('salesforceId');
String updateType = (String)params.get('updateType');
// Find contact
Contact contact;
if (salesforceId != null) {
contact = [SELECT Id FROM Contact WHERE Id = :salesforceId LIMIT 1];
} else if (externalId != null) {
contact = [SELECT Id FROM Contact WHERE External_ID__c = :externalId LIMIT 1];
} else {
throw new APIException('Either salesforceId or externalId must be provided');
}
// Publish platform event
ContactEventPublisher.publishContactUpdate(
contact.Id,
externalId,
updateType
);
// Create response
UpdateResponse response = new UpdateResponse();
response.success = true;
response.contactId = contact.Id;
response.message = 'Update notification sent successfully';
return response;
} catch (Exception e) {
// Handle errors
UpdateResponse response = new UpdateResponse();
response.success = false;
response.message = 'Error processing update: ' + e.getMessage();
// Set appropriate status code
res.statusCode = e instanceof QueryException ? 404 : 400;
return response;
}
}
// Custom exception
class APIException extends Exception {}
// Response class
global class UpdateResponse {
global Boolean success;
global String contactId;
global String message;
}
}
Best Practices
- Use platform events – For scalable, decoupled communication.
- Implement push notifications – Keep UI responsive with real-time updates.
- Consider offline capabilities – Handle intermittent connectivity.
- Use appropriate UX patterns – Toast notifications, badges, etc.
- Optimize for performance – Minimize UI updates to reduce load.
Combining Integration Patterns
Most real-world integration scenarios involve a combination of multiple patterns. Here’s a comprehensive example combining several patterns:
Example: Customer Order System Integration
public class OrderIntegrationService {
// Remote Process Invocation - Request and Reply
// Validate customer credit before placing order
public static CreditCheckResult checkCustomerCredit(String accountId, Decimal orderAmount) {
// Query account to get external customer ID
Account acc = [SELECT Id, External_Customer_ID__c FROM Account WHERE Id = :accountId LIMIT 1];
// Prepare HTTP request
HttpRequest req = new HttpRequest();
req.setEndpoint('https://api.creditsystem.com/check');
req.setMethod('POST');
req.setHeader('Content-Type', 'application/json');
// Prepare request body
Map<String, Object> requestBody = new Map<String, Object>{
'customerId' => acc.External_Customer_ID__c,
'amount' => orderAmount,
'timestamp' => System.now().getTime()
};
req.setBody(JSON.serialize(requestBody));
// Send request
Http http = new Http();
HttpResponse res = http.send(req);
// Process response
if (res.getStatusCode() == 200) {
Map<String, Object> responseData = (Map<String, Object>)JSON.deserializeUntyped(res.getBody());
CreditCheckResult result = new CreditCheckResult();
result.approved = (Boolean)responseData.get('approved');
result.creditLimit = (Decimal)responseData.get('creditLimit');
result.availableCredit = (Decimal)responseData.get('availableCredit');
result.riskScore = (Integer)responseData.get('riskScore');
return result;
} else {
throw new OrderIntegrationException('Credit check failed: ' + res.getStatus());
}
}
// Remote Process Invocation - Fire and Forget
// Notify inventory system about new order
@future(callout=true)
public static void notifyInventorySystem(String orderId) {
// Query order details
Order__c order = [SELECT Id, Name, Account__c, Account__r.External_Customer_ID__c,
Order_Total__c, Status__c
FROM Order__c WHERE Id = :orderId LIMIT 1];
// Query order items
List<Order_Item__c> items = [SELECT Id, Product__c, Product__r.SKU__c,
Quantity__c, Unit_Price__c
FROM Order_Item__c WHERE Order__c = :orderId];
// Prepare inventory items
List<Map<String, Object>> inventoryItems = new List<Map<String, Object>>();
for (Order_Item__c item : items) {
inventoryItems.add(new Map<String, Object>{
'sku' => item.Product__r.SKU__c,
'quantity' => item.Quantity__c
});
}
// Prepare HTTP request
HttpRequest req = new HttpRequest();
req.setEndpoint('https://api.inventory.com/reserve');
req.setMethod('POST');
req.setHeader('Content-Type', 'application/json');
// Prepare request body
Map<String, Object> requestBody = new Map<String, Object>{
'orderId' => order.Name,
'customerId' => order.Account__r.External_Customer_ID__c,
'orderTotal' => order.Order_Total__c,
'items' => inventoryItems,
'timestamp' => System.now().getTime()
};
req.setBody(JSON.serialize(requestBody));
// Send request (fire and forget)
Http http = new Http();
try {
HttpResponse res = http.send(req);
// Log response for monitoring
System.debug('Inventory notification response: ' + res.getStatusCode() + ' ' + res.getStatus());
// Update order status based on response
if (res.getStatusCode() == 200) {
Map<String, Object> responseData = (Map<String, Object>)JSON.deserializeUntyped(res.getBody());
Boolean success = (Boolean)responseData.get('success');
if (success) {
// Update order status
order.Status__c = 'Inventory Reserved';
update order;
// Publish event for UI update
publishOrderStatusEvent(orderId, 'Inventory Reserved');
}
}
} catch (Exception e) {
System.debug('Error notifying inventory system: ' + e.getMessage());
// Log error for retry
logIntegrationError('Inventory', orderId, 'NOTIFY', e.getMessage());
}
}
// UI Update Based on Data Changes
// Publish order status update event
private static void publishOrderStatusEvent(String orderId, String status) {
Order_Status_Update__e event = new Order_Status_Update__e(
Order_Id__c = orderId,
Status__c = status,
Update_Time__c = System.now()
);
EventBus.publish(event);
}
// Batch Data Synchronization
// Schedule order sync with external system
public class OrderSyncScheduler implements Schedulable {
public void execute(SchedulableContext sc) {
// Start batch sync process
Database.executeBatch(new OrderSyncBatch(), 200);
}
}
// Batch class for order synchronization
public class OrderSyncBatch implements Database.Batchable<sObject>, Database.AllowsCallouts {
public Database.QueryLocator start(Database.BatchableContext bc) {
// Query orders that need sync
return Database.getQueryLocator(
'SELECT Id, Name, Account__c, Account__r.External_Customer_ID__c, ' +
'Order_Total__c, Status__c, CreatedDate, LastModifiedDate, ' +
'External_Order_ID__c, Last_Sync_Time__c ' +
'FROM Order__c ' +
'WHERE Status__c NOT IN (\'Completed\', \'Cancelled\') ' +
'AND (Last_Sync_Time__c = NULL OR LastModifiedDate > Last_Sync_Time__c)'
);
}
public void execute(Database.BatchableContext bc, List<Order__c> orders) {
List<Order__c> ordersToUpdate = new List<Order__c>();
// Process each order
for (Order__c order : orders) {
try {
// Sync order to external system
String externalOrderId = syncOrderToExternalSystem(order);
// Update order with sync information
order.External_Order_ID__c = externalOrderId;
order.Last_Sync_Time__c = System.now();
ordersToUpdate.add(order);
} catch (Exception e) {
// Log error
logIntegrationError('OrderSync', order.Id, 'SYNC', e.getMessage());
}
}
// Update orders
if (!ordersToUpdate.isEmpty()) {
update ordersToUpdate;
}
}
public void finish(Database.BatchableContext bc) {
// Send notification that sync is complete
Messaging.SingleEmailMessage mail = new Messaging.SingleEmailMessage();
String[] toAddresses = new String[] {'[email protected]'};
mail.setToAddresses(toAddresses);
mail.setSubject('Order Sync Completed');
mail.setPlainTextBody('The order synchronization batch job has completed.');
Messaging.sendEmail(new Messaging.SingleEmailMessage[] { mail });
}
// Method to sync order to external system
private String syncOrderToExternalSystem(Order__c order) {
// Implement order sync logic here
// Return external order ID
return 'EXT-' + order.Name;
}
}
// Remote Call-In
// REST API to receive order updates from external system
@RestResource(urlMapping='/api/v1/orderUpdate/*')
global class OrderUpdateAPI {
@HttpPost
global static OrderUpdateResponse updateOrder() {
RestRequest req = RestContext.request;
RestResponse res = RestContext.response;
try {
// Parse request
String requestBody = req.requestBody.toString();
Map<String, Object> params = (Map<String, Object>)JSON.deserializeUntyped(requestBody);
// Get order information
String externalOrderId = (String)params.get('externalOrderId');
String newStatus = (String)params.get('status');
String trackingNumber = (String)params.get('trackingNumber');
// Find order by external ID
Order__c order = [SELECT Id FROM Order__c
WHERE External_Order_ID__c = :externalOrderId LIMIT 1];
// Update order
order.Status__c = newStatus;
if (trackingNumber != null) {
order.Tracking_Number__c = trackingNumber;
}
update order;
// Publish event for UI update
publishOrderStatusEvent(order.Id, newStatus);
// Return success response
OrderUpdateResponse response = new OrderUpdateResponse();
response.success = true;
response.orderId = order.Id;
response.message = 'Order updated successfully';
return response;
} catch (Exception e) {
// Return error response
OrderUpdateResponse response = new OrderUpdateResponse();
response.success = false;
response.message = 'Error updating order: ' + e.getMessage();
res.statusCode = e instanceof QueryException ? 404 : 400;
return response;
}
}
// Response class
global class OrderUpdateResponse {
global Boolean success;
global String orderId;
global String message;
}
}
// Helper methods
private static void logIntegrationError(String system, String recordId,
String operation, String errorMessage) {
Integration_Error__c error = new Integration_Error__c(
System__c = system,
Record_Id__c = recordId,
Operation__c = operation,
Error_Message__c = errorMessage,
Error_Time__c = System.now()
);
insert error;
}
// Result class for credit check
public class CreditCheckResult {
public Boolean approved;
public Decimal creditLimit;
public Decimal availableCredit;
public Integer riskScore;
}
// Custom exception
public class OrderIntegrationException extends Exception {}
}
Best Practices for Salesforce Integration
1. Security Considerations
Always implement proper security measures for your integrations:
- Use OAuth 2.0 for authentication
- Implement IP restrictions
- Use SSL/TLS for secure communications
- Follow the principle of least privilege
- Regularly audit connected apps and API usage
2. Error Handling and Resilience
Robust error handling is crucial for production integrations:
- Implement comprehensive exception handling
- Log errors with sufficient context
- Design resilient systems with retry mechanisms
- Set up monitoring and alerting
- Consider using a dedicated error logging object
3. Performance Optimization
Optimize your integrations for performance:
- Minimize API calls through batching
- Use bulk operations when possible
- Implement caching strategies
- Schedule heavy processing during off-peak hours
- Monitor and tune performance regularly
4. Testing and Deployment
Ensure proper testing of integrations:
- Create mock services for testing
- Implement integration tests
- Set up a proper CI/CD pipeline
- Use sandbox environments for testing before production
- Document test cases and expected outcomes
5. Monitoring and Maintenance
Set up proper monitoring for your integrations:
- Implement logging at key points
- Set up health checks for external systems
- Use dashboards to visualize integration metrics
- Set up alerts for integration failures
- Regularly review integration logs and metrics
Additional Resources
- Salesforce Integration Patterns and Practices
- Salesforce API Documentation
- Salesforce Apex Developer Guide
- Lightning Web Components Developer Guide

One thought on “Salesforce Integration Design Patterns: Comprehensive Guide”
Comments are closed.