Speaking twice at Dreamforce · Sept 15-17 →

Guide

Salesforce integration patterns. Pick from the failure mode.

The wrong pattern does not fail loudly. It creates a second source of truth, and the first symptom is two reports that disagree.

Get the written assessment

Certified Partner since 2010 · MVP Hall of Fame · 200+ agents in production · UAE and US desks

The decision that hurts later is ownership

The integration decision that costs you is rarely the protocol. It is ownership. If nobody names which system owns the customer, the invoice, and the order, you end up with two records that both look authoritative, and you find out in a board meeting when two reports disagree.

Pattern follows failure mode, not fashion. Request-reply when a person is waiting for the answer. Batch when the volume is large and the clock is forgiving. Events when the other system must react but must not block yours. Point-to-point is honest when there is genuinely one pipe; MuleSoft earns its invoice when you will reuse the asset. The patterns below include the Apex and the error handling, because an integration nobody can debug at 2 a.m. is not in production. If you want us to choose for your stack, we start by mapping the systems and the fields that must not drift.

Core Integration Patterns

Request-Reply Pattern

The Request-Reply pattern is ideal for real-time, synchronous integrations where immediate response is required.

Implementation Example: REST API Integration

public class ExternalSystemService {
    
    @future(callout=true)
    public static void syncAccountToERP(String accountId) {
        Account acc = [
            SELECT Id, Name, BillingAddress, Industry, AnnualRevenue 
            FROM Account 
            WHERE Id = :accountId
        ];
        
        // Prepare request
        HttpRequest req = new HttpRequest();
        req.setEndpoint('callout:ERP_System/api/customers');
        req.setMethod('POST');
        req.setHeader('Content-Type', 'application/json');
        req.setHeader('Authorization', 'Bearer {!$Credential.ERP_System.token}');
        
        // Build request body
        Map<String, Object> requestBody = new Map<String, Object>{
            'externalId' => acc.Id,
            'companyName' => acc.Name,
            'industry' => acc.Industry,
            'annualRevenue' => acc.AnnualRevenue,
            'address' => new Map<String, Object>{
                'street' => acc.BillingStreet,
                'city' => acc.BillingCity,
                'state' => acc.BillingState,
                'postalCode' => acc.BillingPostalCode,
                'country' => acc.BillingCountry
            }
        };
        
        req.setBody(JSON.serialize(requestBody));
        req.setTimeout(120000); // 2 minute timeout
        
        try {
            Http http = new Http();
            HttpResponse res = http.send(req);
            
            if (res.getStatusCode() == 200 || res.getStatusCode() == 201) {
                // Parse response
                Map<String, Object> responseBody = 
                    (Map<String, Object>) JSON.deserializeUntyped(res.getBody());
                
                // Update Account with ERP ID
                acc.ERP_Customer_ID__c = (String) responseBody.get('customerId');
                acc.ERP_Sync_Status__c = 'Synced';
                acc.ERP_Last_Sync__c = DateTime.now();
                update acc;
                
                // Log success
                createIntegrationLog(acc.Id, 'Success', res.getBody());
            } else {
                // Handle error
                createIntegrationLog(acc.Id, 'Error', res.getBody());
                throw new IntegrationException(
                    'ERP sync failed: ' + res.getStatus()
                );
            }
        } catch (Exception e) {
            createIntegrationLog(acc.Id, 'Exception', e.getMessage());
            throw e;
        }
    }
}

Fire-and-Forget Pattern

Use this pattern for asynchronous operations where immediate response isn't required.

Platform Events Implementation

// Define Platform Event
// API Name: Account_Change_Event__e
// Fields: Account_Id__c, Change_Type__c, Changed_Fields__c

// Publisher Class
public class AccountEventPublisher {
    
    public static void publishAccountChanges(
        List<Account> newAccounts, 
        Map<Id, Account> oldAccountsMap
    ) {
        List<Account_Change_Event__e> events = new List<Account_Change_Event__e>();
        
        for (Account acc : newAccounts) {
            Account oldAcc = oldAccountsMap?.get(acc.Id);
            
            // Detect changes
            Set<String> changedFields = new Set<String>();
            if (oldAcc != null) {
                if (acc.Name != oldAcc.Name) changedFields.add('Name');
                if (acc.Industry != oldAcc.Industry) changedFields.add('Industry');
                if (acc.AnnualRevenue != oldAcc.AnnualRevenue) changedFields.add('AnnualRevenue');
            }
            
            if (!changedFields.isEmpty() || oldAcc == null) {
                Account_Change_Event__e event = new Account_Change_Event__e(
                    Account_Id__c = acc.Id,
                    Change_Type__c = oldAcc == null ? 'INSERT' : 'UPDATE',
                    Changed_Fields__c = String.join(new List<String>(changedFields), ',')
                );
                events.add(event);
            }
        }
        
        if (!events.isEmpty()) {
            List<Database.SaveResult> results = EventBus.publish(events);
            
            // Check publishing results
            for (Database.SaveResult sr : results) {
                if (!sr.isSuccess()) {
                    // Log publishing errors
                    System.debug('Event publishing failed: ' + sr.getErrors()[0].getMessage());
                }
            }
        }
    }
}

// Subscriber (External System)
// Subscribe to: /event/Account_Change_Event__e
// Example Node.js subscriber using jsforce

javascript
const jsforce = require('jsforce');

const conn = new jsforce.Connection({
loginUrl: 'https://login.salesforce.com',
version: '57.0'
});

conn.login(username, password, (err, userInfo) => {
if (err) return console.error(err);

// Subscribe to platform event
const subscription = conn.streaming.topic("/event/Account_Change_Event__e")
.subscribe((message) => {
// account change received

// Process the change
processAccountChange({
accountId: message.payload.Account_Id__c,
changeType: message.payload.Change_Type__c,
changedFields: message.payload.Changed_Fields__c
});
});

subscription.on('error', (error) => {
console.error('Subscription error:', error);
});
});
```

Batch Data Synchronization

Batch Integration Patterns

For large-scale data synchronization, batch processing provides optimal performance and resource utilization.

Bulk API 2.0 Implementation

// Batch Apex for Data Export
public class AccountDataExportBatch implements Database.Batchable<sObject>, 
    Database.Stateful, Database.AllowsCallouts {
    
    private String query;
    private String targetSystem;
    private List<String> errors = new List<String>();
    
    public AccountDataExportBatch(String query, String targetSystem) {
        this.query = query;
        this.targetSystem = targetSystem;
    }
    
    public Database.QueryLocator start(Database.BatchableContext bc) {
        return Database.getQueryLocator(query);
    }
    
    public void execute(Database.BatchableContext bc, List<Account> scope) {
        // Prepare bulk data
        List<Map<String, Object>> bulkData = new List<Map<String, Object>>();
        
        for (Account acc : scope) {
            Map<String, Object> record = new Map<String, Object>{
                'salesforce_id' => acc.Id,
                'name' => acc.Name,
                'industry' => acc.Industry,
                'annual_revenue' => acc.AnnualRevenue,
                'number_of_employees' => acc.NumberOfEmployees,
                'website' => acc.Website,
                'phone' => acc.Phone,
                'billing_address' => new Map<String, Object>{
                    'street' => acc.BillingStreet,
                    'city' => acc.BillingCity,
                    'state' => acc.BillingState,
                    'postal_code' => acc.BillingPostalCode,
                    'country' => acc.BillingCountry
                },
                'last_modified_date' => acc.LastModifiedDate
            };
            bulkData.add(record);
        }
        
        // Send to external system
        try {
            HttpRequest req = new HttpRequest();
            req.setEndpoint('callout:' + targetSystem + '/api/bulk/accounts');
            req.setMethod('POST');
            req.setHeader('Content-Type', 'application/json');
            req.setBody(JSON.serialize(bulkData));
            req.setTimeout(120000);
            
            Http http = new Http();
            HttpResponse res = http.send(req);
            
            if (res.getStatusCode() != 200) {
                errors.add('Batch failed: ' + res.getBody());
            }
        } catch (Exception e) {
            errors.add('Exception in batch: ' + e.getMessage());
        }
    }
    
    public void finish(Database.BatchableContext bc) {
        // Send summary email
        Messaging.SingleEmailMessage mail = new Messaging.SingleEmailMessage();
        mail.setToAddresses(new String[] { UserInfo.getUserEmail() });
        mail.setSubject('Account Data Export Completed');
        
        String body = 'Export to ' + targetSystem + ' completed.\n\n';
        if (!errors.isEmpty()) {
            body += 'Errors encountered:\n' + String.join(errors, '\n');
        } else {
            body += 'All batches processed successfully.';
        }
        
        mail.setPlainTextBody(body);
        Messaging.sendEmail(new Messaging.SingleEmailMessage[] { mail });
    }
}

// Schedule the batch job
AccountDataExportBatch batch = new AccountDataExportBatch(
    'SELECT Id, Name, Industry, AnnualRevenue, NumberOfEmployees, ' +
    'Website, Phone, BillingStreet, BillingCity, BillingState, ' +
    'BillingPostalCode, BillingCountry, LastModifiedDate ' +
    'FROM Account WHERE LastModifiedDate >= LAST_N_DAYS:1',
    'ERP_System'
);
Database.executeBatch(batch, 200);

Change Data Capture (CDC) Pattern

// Enable CDC for Account object in Setup
// Subscribe to change events

// CDC Event Handler
public class AccountCDCHandler {
    
    public static void handleChanges(List<AccountChangeEvent> changes) {
        List<Integration_Queue__c> queueItems = new List<Integration_Queue__c>();
        
        for (AccountChangeEvent event : changes) {
            // Get change header
            EventBus.ChangeEventHeader header = event.ChangeEventHeader;
            
            // Process based on change type
            String changeType = header.changeType;
            List<String> recordIds = header.recordIds;
            
            for (String recordId : recordIds) {
                Integration_Queue__c queueItem = new Integration_Queue__c(
                    Record_Id__c = recordId,
                    Object_Type__c = 'Account',
                    Operation__c = changeType,
                    Status__c = 'Pending',
                    Changed_Fields__c = String.join(header.changedFields, ','),
                    Retry_Count__c = 0
                );
                queueItems.add(queueItem);
            }
        }
        
        if (!queueItems.isEmpty()) {
            insert queueItems;
            
            // Trigger queue processor
            System.enqueueJob(new IntegrationQueueProcessor(queueItems[0].Id));
        }
    }
}

API Design and Governance

RESTful API Design

Custom REST API Implementation

@RestResource(urlMapping='/api/v1/accounts/*')
global with sharing class AccountRestService {
    
    @HttpGet
    global static AccountWrapper getAccount() {
        RestRequest req = RestContext.request;
        String accountId = req.requestURI.substringAfterLast('/');
        
        try {
            Account acc = [
                SELECT Id, Name, Industry, AnnualRevenue, Website,
                    (SELECT Id, Name, Email, Title FROM Contacts LIMIT 5)
                FROM Account 
                WHERE Id = :accountId
            ];
            
            return new AccountWrapper(acc);
        } catch (Exception e) {
            RestResponse res = RestContext.response;
            res.statusCode = 404;
            res.responseBody = Blob.valueOf(JSON.serialize(
                new ErrorResponse('Account not found', e.getMessage())
            ));
            return null;
        }
    }
    
    @HttpPost
    global static ResponseWrapper createAccount(AccountWrapper accountData) {
        try {
            // Validate input
            ValidationResult validation = validateAccountData(accountData);
            if (!validation.isValid) {
                RestResponse res = RestContext.response;
                res.statusCode = 400;
                return new ResponseWrapper(false, validation.errors);
            }
            
            // Create account
            Account acc = new Account(
                Name = accountData.name,
                Industry = accountData.industry,
                AnnualRevenue = accountData.annualRevenue,
                Website = accountData.website
            );
            insert acc;
            
            // Create contacts if provided
            if (accountData.contacts != null && !accountData.contacts.isEmpty()) {
                List<Contact> contacts = new List<Contact>();
                for (ContactWrapper cw : accountData.contacts) {
                    contacts.add(new Contact(
                        AccountId = acc.Id,
                        FirstName = cw.firstName,
                        LastName = cw.lastName,
                        Email = cw.email,
                        Title = cw.title
                    ));
                }
                insert contacts;
            }
            
            RestResponse res = RestContext.response;
            res.statusCode = 201;
            res.addHeader('Location', '/api/v1/accounts/' + acc.Id);
            
            return new ResponseWrapper(true, 'Account created successfully', acc.Id);
        } catch (Exception e) {
            RestResponse res = RestContext.response;
            res.statusCode = 500;
            return new ResponseWrapper(false, e.getMessage());
        }
    }
    
    @HttpPatch
    global static ResponseWrapper updateAccount(Map<String, Object> fields) {
        RestRequest req = RestContext.request;
        String accountId = req.requestURI.substringAfterLast('/');
        
        try {
            Account acc = [SELECT Id FROM Account WHERE Id = :accountId];
            
            // Dynamic field update
            for (String fieldName : fields.keySet()) {
                // Validate field exists and is updateable
                Schema.SObjectField field = Schema.Account.sObjectType
                    .getDescribe().fields.getMap().get(fieldName);
                    
                if (field != null && field.getDescribe().isUpdateable()) {
                    acc.put(fieldName, fields.get(fieldName));
                } else {
                    throw new ApiException('Invalid or non-updateable field: ' + fieldName);
                }
            }
            
            update acc;
            return new ResponseWrapper(true, 'Account updated successfully');
        } catch (Exception e) {
            RestResponse res = RestContext.response;
            res.statusCode = 400;
            return new ResponseWrapper(false, e.getMessage());
        }
    }
    
    // Wrapper classes
    global class AccountWrapper {
        public String id;
        public String name;
        public String industry;
        public Decimal annualRevenue;
        public String website;
        public List<ContactWrapper> contacts;
        
        public AccountWrapper(Account acc) {
            this.id = acc.Id;
            this.name = acc.Name;
            this.industry = acc.Industry;
            this.annualRevenue = acc.AnnualRevenue;
            this.website = acc.Website;
            this.contacts = new List<ContactWrapper>();
            
            for (Contact c : acc.Contacts) {
                this.contacts.add(new ContactWrapper(c));
            }
        }
    }
    
    global class ContactWrapper {
        public String id;
        public String firstName;
        public String lastName;
        public String email;
        public String title;
        
        public ContactWrapper(Contact c) {
            this.id = c.Id;
            this.firstName = c.FirstName;
            this.lastName = c.LastName;
            this.email = c.Email;
            this.title = c.Title;
        }
    }
}

API Versioning Strategy

// Version 2 of the API with backward compatibility
@RestResource(urlMapping='/api/v2/accounts/*')
global with sharing class AccountRestServiceV2 {
    
    @HttpGet
    global static AccountWrapperV2 getAccount() {
        // Include additional fields and relationships
        RestRequest req = RestContext.request;
        String accountId = req.requestURI.substringAfterLast('/');
        
        // Check for API version header
        String apiVersion = req.headers.get('API-Version');
        if (apiVersion == '1.0') {
            // Return V1 format for backward compatibility
            return convertToV1Format(getAccountData(accountId));
        }
        
        // Return V2 format by default
        return getAccountData(accountId);
    }
}

Security and Authentication Patterns

OAuth 2.0 Implementation

Connected App Configuration

<!-- Connected App Settings -->
<connectedApp>
    <contactEmail>admin@company.com</contactEmail>
    <label>External System Integration</label>
    <oauthConfig>
        <callbackUrl>https://external-system.com/oauth/callback</callbackUrl>
        <scopes>
            <scope>api</scope>
            <scope>refresh_token</scope>
            <scope>offline_access</scope>
        </scopes>
        <isSecretRequired>true</isSecretRequired>
    </oauthConfig>
    <oauthPolicy>
        <ipRelaxation>ENFORCE</ipRelaxation>
        <refreshTokenPolicy>infinite</refreshTokenPolicy>
    </oauthPolicy>
</connectedApp>

JWT Bearer Flow Implementation

public class JWTBearerFlow {
    
    public static String getAccessToken() {
        // JWT Header
        Map<String, Object> header = new Map<String, Object>{
            'alg' => 'RS256',
            'typ' => 'JWT'
        };
        
        // JWT Claims
        Map<String, Object> claims = new Map<String, Object>{
            'iss' => '3MVG9...', // Consumer Key
            'sub' => 'integration@company.com', // Username
            'aud' => 'https://login.salesforce.com',
            'exp' => DateTime.now().addMinutes(5).getTime() / 1000
        };
        
        // Generate JWT
        String jwt = generateJWT(header, claims);
        
        // Request access token
        HttpRequest req = new HttpRequest();
        req.setEndpoint('https://login.salesforce.com/services/oauth2/token');
        req.setMethod('POST');
        req.setBody('grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer' +
                   '&assertion=' + jwt);
        
        Http http = new Http();
        HttpResponse res = http.send(req);
        
        if (res.getStatusCode() == 200) {
            Map<String, Object> response = 
                (Map<String, Object>) JSON.deserializeUntyped(res.getBody());
            return (String) response.get('access_token');
        } else {
            throw new AuthException('JWT authentication failed: ' + res.getBody());
        }
    }
    
    private static String generateJWT(
        Map<String, Object> header, 
        Map<String, Object> claims
    ) {
        // Implementation details for JWT generation
        // Typically involves signing with private key
        return 'generated.jwt.token';
    }
}

API Security Best Practices

@RestResource(urlMapping='/api/secure/data/*')
global with sharing class SecureDataService {
    
    @HttpPost
    global static ResponseWrapper processData(RequestWrapper request) {
        try {
            // 1. Validate authentication
            if (!validateApiKey()) {
                return new ResponseWrapper(401, 'Unauthorized');
            }
            
            // 2. Check rate limits
            if (!checkRateLimit()) {
                return new ResponseWrapper(429, 'Rate limit exceeded');
            }
            
            // 3. Validate input
            ValidationResult validation = validateInput(request);
            if (!validation.isValid) {
                return new ResponseWrapper(400, validation.errors);
            }
            
            // 4. Check permissions
            if (!hasPermission(request.operation)) {
                return new ResponseWrapper(403, 'Forbidden');
            }
            
            // 5. Process request with encryption
            String encryptedData = Crypto.encryptWithManagedIV(
                'AES256',
                Blob.valueOf(getEncryptionKey()),
                Blob.valueOf(JSON.serialize(request.data))
            ).toString();
            
            // 6. Audit trail
            createAuditLog(request, 'Success');
            
            return new ResponseWrapper(200, 'Success', encryptedData);
            
        } catch (Exception e) {
            createAuditLog(request, 'Error: ' + e.getMessage());
            return new ResponseWrapper(500, 'Internal server error');
        }
    }
    
    private static Boolean validateApiKey() {
        String apiKey = RestContext.request.headers.get('X-API-Key');
        
        // Validate against Custom Metadata Type
        API_Key__mdt keyRecord = [
            SELECT Id, Is_Active__c, Expiration_Date__c
            FROM API_Key__mdt
            WHERE Key__c = :apiKey
            AND Is_Active__c = true
            LIMIT 1
        ];
        
        return keyRecord != null && 
               (keyRecord.Expiration_Date__c == null || 
                keyRecord.Expiration_Date__c > Date.today());
    }
    
    private static Boolean checkRateLimit() {
        String clientId = RestContext.request.headers.get('X-Client-Id');
        
        // Check requests in last hour
        Integer requestCount = [
            SELECT COUNT()
            FROM API_Request_Log__c
            WHERE Client_Id__c = :clientId
            AND CreatedDate = LAST_N_HOURS:1
        ];
        
        return requestCount < 1000; // 1000 requests per hour limit
    }
}

Error Handling and Monitoring

Error handling someone can read at 2 a.m.

Retry Pattern Implementation

public class IntegrationRetryHandler implements Queueable, Database.AllowsCallouts {
    
    private Integration_Queue__c queueItem;
    private static final Integer MAX_RETRIES = 3;
    private static final List<Integer> RETRY_INTERVALS = new List<Integer>{
        1, 5, 15 // Minutes
    };
    
    public IntegrationRetryHandler(Id queueItemId) {
        this.queueItem = [
            SELECT Id, Record_Id__c, Object_Type__c, Operation__c, 
                   Payload__c, Retry_Count__c, Status__c
            FROM Integration_Queue__c
            WHERE Id = :queueItemId
        ];
    }
    
    public void execute(QueueableContext context) {
        try {
            // Attempt integration
            IntegrationResult result = performIntegration();
            
            if (result.isSuccess) {
                queueItem.Status__c = 'Completed';
                queueItem.Completed_Date__c = DateTime.now();
            } else {
                handleFailure(result);
            }
            
            update queueItem;
            
        } catch (Exception e) {
            handleException(e);
        }
    }
    
    private void handleFailure(IntegrationResult result) {
        queueItem.Last_Error__c = result.errorMessage;
        queueItem.Retry_Count__c = queueItem.Retry_Count__c + 1;
        
        if (queueItem.Retry_Count__c < MAX_RETRIES) {
            // Schedule retry
            Integer delayMinutes = RETRY_INTERVALS[Integer.valueOf(queueItem.Retry_Count__c) - 1];
            
            queueItem.Status__c = 'Retry Scheduled';
            queueItem.Next_Retry__c = DateTime.now().addMinutes(delayMinutes);
            
            // Enqueue with delay
            System.enqueueJob(
                new IntegrationRetryHandler(queueItem.Id), 
                delayMinutes
            );
        } else {
            // Max retries reached
            queueItem.Status__c = 'Failed';
            notifyAdministrators();
        }
    }
    
    private void handleException(Exception e) {
        queueItem.Status__c = 'Error';
        queueItem.Last_Error__c = e.getTypeName() + ': ' + e.getMessage();
        queueItem.Stack_Trace__c = e.getStackTraceString();
        
        update queueItem;
        
        // Create platform event for real-time monitoring
        Integration_Error_Event__e errorEvent = new Integration_Error_Event__e(
            Queue_Item_Id__c = queueItem.Id,
            Error_Message__c = e.getMessage(),
            Stack_Trace__c = e.getStackTraceString(),
            Severity__c = 'High'
        );
        
        EventBus.publish(errorEvent);
    }
}

Integration Monitoring Dashboard

public class IntegrationMonitoringService {
    
    @AuraEnabled(cacheable=true)
    public static IntegrationMetrics getIntegrationMetrics() {
        IntegrationMetrics metrics = new IntegrationMetrics();
        
        // Last 24 hours statistics
        DateTime last24Hours = DateTime.now().addHours(-24);
        
        // Success rate
        AggregateResult[] results = [
            SELECT Status__c, COUNT(Id) cnt
            FROM Integration_Queue__c
            WHERE CreatedDate >= :last24Hours
            GROUP BY Status__c
        ];
        
        for (AggregateResult ar : results) {
            String status = (String) ar.get('Status__c');
            Integer count = (Integer) ar.get('cnt');
            
            if (status == 'Completed') {
                metrics.successCount = count;
            } else if (status == 'Failed') {
                metrics.failureCount = count;
            } else if (status == 'Retry Scheduled' || status == 'Pending') {
                metrics.pendingCount = count;
            }
        }
        
        // Average response time
        AggregateResult avgResult = [
            SELECT AVG(Response_Time__c) avgTime
            FROM Integration_Log__c
            WHERE CreatedDate >= :last24Hours
            AND Status__c = 'Success'
        ];
        
        metrics.averageResponseTime = avgResult.get('avgTime') != null ? 
            (Decimal) avgResult.get('avgTime') : 0;
        
        // Error trends
        metrics.errorTrends = getErrorTrends();
        
        return metrics;
    }
    
    public class IntegrationMetrics {
        @AuraEnabled public Integer successCount { get; set; }
        @AuraEnabled public Integer failureCount { get; set; }
        @AuraEnabled public Integer pendingCount { get; set; }
        @AuraEnabled public Decimal averageResponseTime { get; set; }
        @AuraEnabled public List<ErrorTrend> errorTrends { get; set; }
        
        public IntegrationMetrics() {
            this.successCount = 0;
            this.failureCount = 0;
            this.pendingCount = 0;
            this.averageResponseTime = 0;
            this.errorTrends = new List<ErrorTrend>();
        }
    }
}

The brief

Send the messy version.

The honest no: if the work is a software factory with a named delivery date, we are the wrong partner.