Introduction: Why Apex Performance Matters in Multi-Tenant Salesforce
Salesforce's multi-tenant architecture enforces strict governor limits — every Apex transaction shares compute resources with thousands of other organisations on the same infrastructure. Poorly optimised Apex code doesn't just slow down your org; it can trigger governor limit exceptions that crash entire business processes.
Understanding and designing around these constraints is fundamental to building scalable, production-grade Salesforce applications. This guide covers the complete spectrum of Apex performance optimisation: from SOQL query patterns and DML bulkification through trigger frameworks, async processing, and CI/CD-integrated performance monitoring.
Governor Limits: The Complete Reference for Developers
Know your transaction limits to design within them:
- SOQL Limits: 100 SOQL queries per synchronous transaction (200 for async) — each query counts regardless of row count. Aggregate queries (COUNT, SUM, AVG) count as regular SOQL queries.
- DML Limits: 150 DML statements per transaction — but each statement can process up to 10,000 records. Use collection-based DML (
insert recordList) instead of individual record operations. - Heap Size: 6MB for synchronous code, 12MB for asynchronous — large query results, JSON parsing, and string concatenation are common heap exhaustion causes. Use
Limits.getHeapSize()to monitor. - CPU Time: 10,000ms synchronous, 60,000ms asynchronous — CPU time includes Apex execution, formula field evaluation, and workflow rule processing. Avoid nested loops and recursive calculations.
- Callout Limits: 100 callouts per transaction with 120-second total timeout — each callout has a 120-second individual timeout. Long-running integrations require async patterns.
SOQL Optimisation: Query Patterns That Scale
Write efficient SOQL queries that minimise governor limit consumption:
- Bulkify Queries: Never place SOQL inside loops — collect IDs first, then query once with
WHERE Id IN :idSet. A single bulk query replacing 200 individual queries saves 199 SOQL calls per transaction. - Selective Queries: Add indexed fields to WHERE clauses —
WHERE CreatedDate > :lastMonth AND Status__c = 'Active'. Salesforce Query Optimizer uses indexes on standard fields (Id, Name, CreatedDate) and custom indexed fields. - Relationship Queries: Use parent-to-child (
SELECT Id, (SELECT Id FROM Contacts) FROM Account) and child-to-parent (SELECT Account.Name FROM Contact) queries to combine multiple objects in a single SOQL call. - Aggregate Queries: Use
SELECT COUNT(Id), SUM(Amount) FROM Opportunity GROUP BY StageNameinstead of querying all records and calculating in Apex — the database engine handles aggregation far more efficiently. - Query Plan Analysis: Use the Query Plan tool in Developer Console — check
TableScanvsIndexScanand ensure selectivity thresholds are met. Queries returning more than 30% of total records trigger full table scans.
DML Bulkification: Collection-Based Data Operations
Process records in bulk collections to maximise DML efficiency:
- Collection DML: Always use
insert newRecordsinstead ofinsert singleRecordin loops — a single DML statement handles up to 10,000 records. Build lists first, then execute one DML operation. - Map-Based Processing: Use
Map<Id, Account>to organise records by key —Map<Id, Account> accountMap = new Map<Id, Account>(accountList)enables O(1) lookups instead of O(n) list searches. - Partial Success: Use
Database.insert(records, false)for partial DML — processes valid records and returns errors for failures instead of rolling back the entire operation. Essential for data migration and integration scenarios. - Upsert Operations:
upsert recordList ExternalId__ccombines insert and update logic — matching records by external ID avoids duplicate SOQL queries to check existence before insert/update decisions. - Avoiding Mixed DML: Setup objects (User, Profile) and non-setup objects (Account, Contact) cannot be modified in the same transaction — use
@futuremethods or Queueable Apex to separate these operations.
Trigger Frameworks: Scalable Trigger Architecture
Implement structured trigger patterns for maintainable, testable code:
- One Trigger Per Object: Never create multiple triggers on the same object — execution order is unpredictable. Route all logic through a single trigger that delegates to handler classes.
- Handler Pattern: Separate trigger logic into dedicated handler classes —
AccountTriggerHandler.handleBeforeInsert(Trigger.new). This enables unit testing without DML operations and code reuse across trigger contexts. - Recursion Prevention: Use static boolean flags or Set collections to prevent infinite trigger recursion —
if (TriggerHandler.hasRun) return;. Process each record at most once per transaction. - Context Routing: Route to appropriate methods based on trigger context —
if (Trigger.isBefore && Trigger.isInsert) handler.beforeInsert(). The handler pattern supports all seven trigger contexts (before/after insert, update, delete, undelete). - Custom Metadata Control: Store trigger activation flags in Custom Metadata Types — enable/disable triggers per-object without code deployment. Essential for data migrations and emergency troubleshooting.
Transform Your Publishing Workflow
Our experts can help you build scalable, API-driven publishing systems tailored to your business.
Asynchronous Processing: Future, Queueable, and Batch Apex
Offload heavy processing to async execution contexts:
- @future Methods: Simple async execution for callouts and mixed DML —
@future(callout=true)runs in a separate transaction with expanded limits. Limited to primitive parameters (no sObject arguments). - Queueable Apex: Full-featured async processing with sObject parameters, job chaining, and monitoring —
System.enqueueJob(new ProcessRecordsJob(recordList)). Chain up to 5 jobs for sequential processing pipelines. - Batch Apex: Process millions of records in configurable batch sizes —
Database.executeBatch(new AccountBatch(), 200)processes records in chunks of 200 with separate governor limit scopes per batch. Ideal for data cleansing, mass updates, and archival. - Schedulable Apex: Schedule recurring jobs with cron expressions —
System.schedule('Nightly Sync', '0 0 2 * * ?', new NightlySyncJob()). Combine with Batch Apex for scheduled mass processing. - Platform Events: Publish events for event-driven async processing — subscribers process events in separate transactions with independent governor limits. Ideal for decoupling trigger logic and cross-org communication.
Test Coverage and Performance Testing
Validate performance with bulk-aware test methods:
- Bulk Testing: Always test with 200+ records (the default trigger batch size) —
List<Account> testAccounts = TestDataFactory.createAccounts(200). Tests that pass with 1 record often fail with 200 due to governor limits. - Limits Assertions: Assert governor limit consumption in tests —
System.assert(Limits.getQueries() < 10, 'SOQL queries exceeded threshold'). Catch performance regressions before deployment. - Test Data Factory: Create reusable test data factories with
@TestVisiblemethods — generate consistent test data for accounts, contacts, opportunities, and custom objects. Avoid@isTest(SeeAllData=true)for data isolation. - Negative Testing: Test governor limit boundary conditions — verify graceful handling when approaching limits rather than crashing mid-transaction. Test partial DML success scenarios.
- Integration Tests: Test end-to-end flows including triggers, process builders, and flows — verify that the complete automation stack stays within governor limits when processing bulk data.
Monitoring, Security, and MDS Salesforce Services
Continuously monitor and secure your Apex codebase:
- Debug Logs: Configure debug log levels per user — filter by Apex, SOQL, and callout categories. Analyse execution times and governor limit consumption in Developer Console's Log Inspector.
- Event Monitoring: Enable Salesforce Event Monitoring for production — track Apex execution events, login forensics, and API usage patterns. Set up alerts for governor limit violations exceeding 80% thresholds.
- Static Analysis: Integrate PMD for Apex into CI/CD pipelines — enforce coding standards, detect anti-patterns (SOQL in loops, hardcoded IDs), and calculate cyclomatic complexity. CodeScan provides enterprise-grade static analysis.
- Security Patterns: Always use
with sharingfor record-level security, avoid dynamic SOQL unless parameterised withString.escapeSingleQuotes(), and implementstripInaccessible()for field-level security enforcement in API responses.
MDS provides Salesforce Apex development and optimisation services — from governor limit audits and performance refactoring through trigger framework implementation, async architecture design, and CI/CD pipeline integration with automated performance testing.



