Salesforce Integration Design Patterns: Comprehensive Guide

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:

  1. Remote Process Invocation—Request and Reply
  2. Remote Process Invocation—Fire and Forget
  3. Batch Data Synchronization
  4. Remote Call-In
  5. 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

  1. Handle timeouts and errors gracefully – Always implement proper error handling.
  2. Use named credentials – For secure authentication with external systems.
  3. Consider governor limits – Be mindful of Apex callout limits (100 callouts per transaction).
  4. Implement bulkification – Design your code to handle bulk operations.
  5. 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

  1. Design for failure – Implement error logging and monitoring.
  2. Use platform events – They provide a scalable publish-subscribe model.
  3. Consider retry logic – For critical operations, implement a retry mechanism.
  4. Batch similar requests – Group related operations to reduce API calls.
  5. 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

  1. Schedule during off-peak hours – Minimize impact on system performance.
  2. Implement checkpointing – Store progress in case of failures.
  3. Use batch processing – Handle large data volumes efficiently.
  4. Include detailed logging – Track sync operations for troubleshooting.
  5. 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

  1. Implement proper authentication – Use OAuth, JWT, or API keys.
  2. Apply rate limiting – Prevent abuse of your API endpoints.
  3. Handle bulk operations – Design APIs to handle bulk data efficiently.
  4. Provide comprehensive error messages – Make troubleshooting easier.
  5. 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

  1. Use platform events – For scalable, decoupled communication.
  2. Implement push notifications – Keep UI responsive with real-time updates.
  3. Consider offline capabilities – Handle intermittent connectivity.
  4. Use appropriate UX patterns – Toast notifications, badges, etc.
  5. 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

Latest Posts

Amit Singh
Amit Singh

Amit Singh aka @sfdcpanther/pantherschools, a Salesforce Technical Architect, Consultant with over 8+ years of experience in Salesforce technology. 21x Certified. Blogger, Speaker, and Instructor. DevSecOps Champion

Articles: 299

Newsletter Updates

Enter your email address below and subscribe to our newsletter

One comment

Leave a Reply

Your email address will not be published. Required fields are marked *

Discover more from Panther Schools

Subscribe now to keep reading and get access to the full archive.

Continue reading