Software Engineering & Digital Products for Global Enterprises since 2006
CMMi Level 3SOC 2ISO 27001
Menu
View all services
Staff Augmentation
Embed senior engineers in your team within weeks.
Dedicated Teams
A ring-fenced squad with PM, leads, and engineers.
Build-Operate-Transfer
We hire, run, and transfer the team to you.
Contract-to-Hire
Try the talent. Convert when you're ready.
ForceHQ
Skill testing, interviews and ranking — powered by AI.
RoboRingo
Build, deploy and monitor voice agents without code.
MailGovern
Policy, retention and compliance for enterprise email.
Vishing
Test and train staff against AI-driven voice attacks.
CyberForceHQ
Continuous, adaptive security training for every team.
IDS Load Balancer
Built for Multi Instance InDesign Server, to distribute jobs.
AutoVAPT.ai
AI agent for continuous, automated vulnerability and penetration testing.
Salesforce + InDesign Connector
Bridge Salesforce data into InDesign to design print catalogues at scale.
View all solutions
Banking, Financial Services & Insurance
Cloud, digital and legacy modernisation across financial entities.
Healthcare
Clinical platforms, patient engagement, and connected medical devices.
Pharma & Life Sciences
Trial systems, regulatory data, and field-force enablement.
Professional Services & Education
Workflow automation, learning platforms, and consulting tooling.
Media & Entertainment
AI video processing, OTT platforms, and content workflows.
Technology & SaaS
Product engineering, integrations, and scale for tech companies.
Retail & eCommerce
Shopify, print catalogues, web-to-print, and order automation.
View all industries
Blog
Engineering notes, opinions, and field reports.
Case Studies
How clients shipped — outcomes, stack, lessons.
White Papers
Deep-dives on AI, talent models, and platforms.
Portfolio
Selected work across industries.
View all resources
About Us
Who we are, our story, and what drives us.
Co-Innovation
How we partner to build new products together.
Careers
Open roles and what it's like to work here.
News
Press, announcements, and industry updates.
Leadership
The people steering MetaDesign.
Locations
Gurugram, Brisbane, Detroit and beyond.
Contact Us
Talk to sales, hiring, or partnerships.
Request TalentStart a Project
Enterprise Software

Optimizing Apex Performance in Salesforce: Best Practices & Code Examples

GS
Girish Sagar
Technical Content Lead
May 26, 2025
15 min read
Optimizing Apex Performance in Salesforce: Best Practices & Code Examples — Enterprise Software | MetaDesign Solutions

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 StageName instead 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 TableScan vs IndexScan and 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 newRecords instead of insert singleRecord in 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__c combines 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 @future methods 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.

Book a free consultation

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 @TestVisible methods — 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 sharing for record-level security, avoid dynamic SOQL unless parameterised with String.escapeSingleQuotes(), and implement stripInaccessible() 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.

FAQ

Frequently Asked Questions

Common questions about this topic, answered by our engineering team.

Never place SOQL queries inside loops. Collect IDs into Sets, then query once with WHERE Id IN :idSet. Use relationship queries to combine parent-child data in a single call. Leverage Maps for O(1) record lookups. A single bulk query replacing 200 individual queries saves 199 SOQL calls per transaction.

Key limits: 100 SOQL queries (200 async), 150 DML statements, 6MB heap (12MB async), 10,000ms CPU time (60,000ms async), 100 callouts with 120s total timeout, and 10,000 records per DML statement. Use the Limits class to monitor consumption programmatically.

Use Batch Apex for processing millions of records in configurable chunks — each batch gets fresh governor limits. Use Queueable Apex for smaller async operations that need sObject parameters, job chaining (up to 5 jobs), and real-time monitoring. Batch Apex is for scheduled bulk processing; Queueable is for on-demand async work.

Use static boolean flags or Set<Id> collections in a separate handler class. Check if (TriggerHandler.hasProcessed.contains(recordId)) return; at the start of trigger logic. Also implement the one-trigger-per-object pattern with dedicated handler classes for each trigger context.

PMD for Apex provides static analysis for anti-patterns (SOQL in loops, hardcoded IDs). Apex Replay Debugger traces execution in VS Code. CodeScan integrates with CI/CD pipelines. Developer Console's Query Plan tool identifies non-selective SOQL. Event Monitoring tracks production performance patterns.

Discussion

Join the Conversation

Ready when you are

Let's build something great together.

A 30-minute call with a principal engineer. We'll listen, sketch, and tell you whether we're the right partner — even if the answer is no.

Talk to a strategist
Need help with your project? Let's talk.
Book a call