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.
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:
Let’s explore each pattern in detail.
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.

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;
}
}
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());
}
}
}
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.

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
);
}
}
}
// 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
}
}
}
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.

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);
}
}
@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;
}
}
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.

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;
}
}
@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];
}
}
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.

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>
<!-- 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>
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());
}
}
}
}
@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;
}
}
Most real-world integration scenarios involve a combination of multiple patterns. Here’s a comprehensive example combining several patterns:
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 {}
}
Always implement proper security measures for your integrations:
Robust error handling is crucial for production integrations:
Optimize your integrations for performance:
Ensure proper testing of integrations:
Set up proper monitoring for your integrations:
[…] Salesforce Integration Design Patterns: Comprehensive Guide […]