Matter AI | Code Reviewer Documentation home pagelight logodark logo
  • Contact
  • Github
  • Sign in
  • Sign in
  • Documentation
  • Blog
  • Discord
  • Github
  • Introduction
    • What is Matter AI?
    Getting Started
    • QuickStart
    Product
    • Security Analysis
    • Code Quality
    • Agentic Chat
    • RuleSets
    • Memories
    • Analytics
    • Command List
    • Configurations
    Patterns
    • Languages
      • Supported Languages
      • Python
      • Java
      • JavaScript
      • TypeScript
      • Node.js
      • React
      • Fastify
      • Next.js
      • Terraform
      • C#
      • C++
      • C
      • Go
      • Rust
      • Swift
      • React Native
      • Spring Boot
      • Kotlin
      • Flutter
      • Ruby
      • PHP
      • Scala
      • Perl
      • R
      • Dart
      • Elixir
      • Erlang
      • Haskell
      • Lua
      • Julia
      • Clojure
      • Groovy
      • Fortran
      • COBOL
      • Pascal
      • Assembly
      • Bash
      • PowerShell
      • SQL
      • PL/SQL
      • T-SQL
      • MATLAB
      • Objective-C
      • VBA
      • ABAP
      • Apex
      • Apache Camel
      • Crystal
      • D
      • Delphi
      • Elm
      • F#
      • Hack
      • Lisp
      • OCaml
      • Prolog
      • Racket
      • Scheme
      • Solidity
      • Verilog
      • VHDL
      • Zig
      • MongoDB
      • ClickHouse
      • MySQL
      • GraphQL
      • Redis
      • Cassandra
      • Elasticsearch
    • Security
    • Performance
    Integrations
    • Code Repositories
    • Team Messengers
    • Ticketing
    Enterprise
    • Enterprise Deployment Overview
    • Enterprise Configurations
    • Observability and Fallbacks
    • Create Your Own GitHub App
    • Self-Hosting Options
    • RBAC
    Patterns
    Languages

    Apex

    Apex is a strongly typed, object-oriented programming language that allows developers to execute flow and transaction control statements on the Salesforce platform server in conjunction with calls to the API. It has a Java-like syntax and acts like database stored procedures.

    Apex, despite being a powerful language for Salesforce development, has several common anti-patterns that can lead to performance issues, governor limit violations, maintainability problems, and bugs. Here are the most important anti-patterns to avoid when writing Apex code.

    // Anti-pattern: SOQL query inside a loop
    for (Account acc : [SELECT Id, Name FROM Account LIMIT 100]) {
        // For each account, find its contacts
        List<Contact> contacts = [SELECT Id, FirstName, LastName 
                                 FROM Contact 
                                 WHERE AccountId = :acc.Id];
        
        // Process contacts
        for (Contact con : contacts) {
            // Do something with each contact
        }
    }
    
    // Better approach: Use relationship queries or maps
    // Option 1: Use relationship query
    for (Account acc : [SELECT Id, Name, 
                        (SELECT Id, FirstName, LastName FROM Contacts)
                        FROM Account LIMIT 100]) {
        // Process contacts
        for (Contact con : acc.Contacts) {
            // Do something with each contact
        }
    }
    
    // Option 2: Use a single query with a map
    Map<Id, Account> accountMap = new Map<Id, Account>([SELECT Id, Name FROM Account LIMIT 100]);
    List<Contact> allContacts = [SELECT Id, FirstName, LastName, AccountId 
                               FROM Contact 
                               WHERE AccountId IN :accountMap.keySet()];
    
    // Group contacts by AccountId
    Map<Id, List<Contact>> contactsByAccount = new Map<Id, List<Contact>>();
    for (Contact con : allContacts) {
        if (!contactsByAccount.containsKey(con.AccountId)) {
            contactsByAccount.put(con.AccountId, new List<Contact>());
        }
        contactsByAccount.get(con.AccountId).add(con);
    }
    
    // Process accounts and their contacts
    for (Account acc : accountMap.values()) {
        List<Contact> contacts = contactsByAccount.get(acc.Id);
        if (contacts != null) {
            for (Contact con : contacts) {
                // Do something with each contact
            }
        }
    }

    Never put SOQL queries inside loops. This can quickly exceed the governor limit of 100 SOQL queries per transaction. Instead, use relationship queries to retrieve related records in a single query, or use a map-based approach to fetch all related records in one query and then process them in memory.

    // Anti-pattern: DML inside a loop
    for (Account acc : [SELECT Id, Name FROM Account WHERE Rating = 'Hot']) {
        acc.Description = 'Hot account - high priority';
        update acc; // DML inside loop
    }
    
    // Better approach: Collect records and perform bulk DML
    List<Account> accountsToUpdate = new List<Account>();
    
    for (Account acc : [SELECT Id, Name FROM Account WHERE Rating = 'Hot']) {
        acc.Description = 'Hot account - high priority';
        accountsToUpdate.add(acc);
    }
    
    // Single DML operation outside the loop
    if (!accountsToUpdate.isEmpty()) {
        update accountsToUpdate;
    }

    Avoid performing DML operations (insert, update, delete, upsert) inside loops. This can quickly exceed the governor limit of 150 DML operations per transaction. Instead, collect all records that need to be modified and perform a single bulk DML operation outside the loop.

    // Anti-pattern: Non-bulkified trigger
    trigger ContactTrigger on Contact (after insert) {
        // Process one record at a time
        for (Contact con : Trigger.new) {
            // Query account for each contact
            Account acc = [SELECT Id, Name FROM Account WHERE Id = :con.AccountId];
            
            // Create a task for each contact
            Task t = new Task();
            t.Subject = 'Follow up with new contact';
            t.WhatId = acc.Id;
            t.WhoId = con.Id;
            insert t; // DML inside loop
        }
    }
    
    // Better approach: Bulkified trigger
    trigger ContactTrigger on Contact (after insert) {
        // Collect all Account IDs
        Set<Id> accountIds = new Set<Id>();
        for (Contact con : Trigger.new) {
            if (con.AccountId != null) {
                accountIds.add(con.AccountId);
            }
        }
        
        // Query accounts in bulk
        Map<Id, Account> accountMap = new Map<Id, Account>(
            [SELECT Id, Name FROM Account WHERE Id IN :accountIds]
        );
        
        // Create tasks in bulk
        List<Task> tasksToInsert = new List<Task>();
        for (Contact con : Trigger.new) {
            if (con.AccountId != null && accountMap.containsKey(con.AccountId)) {
                Task t = new Task();
                t.Subject = 'Follow up with new contact';
                t.WhatId = con.AccountId;
                t.WhoId = con.Id;
                tasksToInsert.add(t);
            }
        }
        
        // Insert tasks in bulk
        if (!tasksToInsert.isEmpty()) {
            insert tasksToInsert;
        }
    }

    Always bulkify your triggers to handle multiple records efficiently. Salesforce can process up to 200 records in a single transaction, and your triggers should be designed to handle this scenario without exceeding governor limits. Use bulk queries and DML operations, and avoid processing records one at a time.

    // Anti-pattern: Hardcoding record IDs
    public void assignToDefaultQueue() {
        // Hardcoded ID of a specific queue
        Id queueId = '00G1a000001XgXx';
        
        List<Case> cases = [SELECT Id FROM Case WHERE Status = 'New'];
        for (Case c : cases) {
            c.OwnerId = queueId;
        }
        update cases;
    }
    
    // Better approach: Query for IDs or use custom metadata/settings
    public void assignToDefaultQueue() {
        // Query for the queue ID
        Group defaultQueue = [SELECT Id FROM Group 
                             WHERE Type = 'Queue' AND DeveloperName = 'DefaultCaseQueue' 
                             LIMIT 1];
        
        List<Case> cases = [SELECT Id FROM Case WHERE Status = 'New'];
        for (Case c : cases) {
            c.OwnerId = defaultQueue.Id;
        }
        update cases;
    }
    
    // Even better: Use custom metadata or custom settings
    public void assignToDefaultQueue() {
        // Retrieve queue ID from custom setting
        Queue_Settings__c settings = Queue_Settings__c.getInstance('Default');
        Id queueId = settings.Default_Case_Queue_ID__c;
        
        List<Case> cases = [SELECT Id FROM Case WHERE Status = 'New'];
        for (Case c : cases) {
            c.OwnerId = queueId;
        }
        update cases;
    }

    Never hardcode record IDs in your code. IDs can be different across environments (sandbox, production), and hardcoded IDs make your code environment-specific and brittle. Instead, query for the IDs dynamically or store them in custom metadata types or custom settings.

    // Anti-pattern: Not checking limits
    public void processAccounts() {
        List<Account> accounts = [SELECT Id FROM Account LIMIT 50000];
        // Process accounts without checking limits
    }
    
    // Better approach: Check limits and handle accordingly
    public void processAccounts() {
        // Check if we have enough SOQL queries available
        if (Limits.getQueries() >= Limits.getLimitQueries() - 1) {
            throw new CustomException('Too many SOQL queries used in this transaction');
        }
        
        // Check if we can process all accounts or need to batch
        Integer recordLimit = Limits.getLimitHeapSize() / 10000; // Approximate size per record
        List<Account> accounts = [SELECT Id FROM Account LIMIT :recordLimit];
        
        // Process accounts
    }

    Use the Limits class to check governor limits at runtime and handle situations where you might exceed them. This is especially important for code that processes variable amounts of data or is called from multiple contexts.

    // Anti-pattern: No exception handling
    public void createAccount(String name) {
        Account acc = new Account(Name = name);
        insert acc; // Will throw exception if insert fails
        
        // Create related records
        Contact con = new Contact(LastName = 'Contact for ' + name, AccountId = acc.Id);
        insert con; // Might not be reached if account insert fails
    }
    
    // Better approach: Use try-catch for proper error handling
    public void createAccount(String name) {
        try {
            Account acc = new Account(Name = name);
            insert acc;
            
            // Create related records
            Contact con = new Contact(LastName = 'Contact for ' + name, AccountId = acc.Id);
            insert con;
        } catch (DmlException e) {
            // Log the error
            System.debug('Error creating account: ' + e.getMessage());
            
            // Rethrow as custom exception with more context
            throw new AccountCreationException('Failed to create account: ' + e.getMessage(), e);
        }
    }
    
    // Custom exception class
    public class AccountCreationException extends Exception {}

    Use try-catch blocks to handle exceptions gracefully. This allows you to provide better error messages, perform cleanup operations, and prevent partial transactions. Create custom exception classes for specific error scenarios to make error handling more structured.

    // Anti-pattern: Using System.debug for production logging
    public void processOrders() {
        System.debug('Starting order processing');
        
        List<Order__c> orders = [SELECT Id, Status__c FROM Order__c WHERE Status__c = 'Pending'];
        System.debug('Found ' + orders.size() + ' orders to process');
        
        // Process orders
        for (Order__c ord : orders) {
            try {
                // Process order
                System.debug('Processed order: ' + ord.Id);
            } catch (Exception e) {
                System.debug('Error processing order: ' + e.getMessage());
            }
        }
    }
    
    // Better approach: Use a logging framework or Platform Events
    public void processOrders() {
        Logger.info('Starting order processing');
        
        List<Order__c> orders = [SELECT Id, Status__c FROM Order__c WHERE Status__c = 'Pending'];
        Logger.info('Found ' + orders.size() + ' orders to process');
        
        // Process orders
        for (Order__c ord : orders) {
            try {
                // Process order
                Logger.info('Processed order: ' + ord.Id);
            } catch (Exception e) {
                Logger.error('Error processing order: ' + ord.Id, e);
                
                // Create error log record
                Error_Log__c log = new Error_Log__c(
                    Object_Type__c = 'Order__c',
                    Record_Id__c = ord.Id,
                    Error_Message__c = e.getMessage(),
                    Stack_Trace__c = e.getStackTraceString()
                );
                insert log;
            }
        }
    }

    Don’t rely on System.debug for production logging. Debug logs are not easily accessible in production and are only retained for a limited time. Instead, use a custom logging framework, custom objects to store logs, or Platform Events for more robust logging in production environments.

    // Anti-pattern: Not using Test.startTest() and Test.stopTest()
    @isTest
    private class OrderServiceTest {
        @isTest
        static void testProcessOrders() {
            // Create test data
            List<Order__c> testOrders = createTestOrders(100);
            insert testOrders;
            
            // Call the method to test
            OrderService service = new OrderService();
            service.processOrders();
            
            // Verify results
            List<Order__c> processedOrders = [SELECT Id, Status__c FROM Order__c WHERE Status__c = 'Processed'];
            System.assertEquals(100, processedOrders.size());
        }
    }
    
    // Better approach: Use Test.startTest() and Test.stopTest()
    @isTest
    private class OrderServiceTest {
        @isTest
        static void testProcessOrders() {
            // Create test data
            List<Order__c> testOrders = createTestOrders(100);
            insert testOrders;
            
            // Reset governor limits
            Test.startTest();
            
            // Call the method to test
            OrderService service = new OrderService();
            service.processOrders();
            
            // Force async processes to complete and restore governor limits
            Test.stopTest();
            
            // Verify results
            List<Order__c> processedOrders = [SELECT Id, Status__c FROM Order__c WHERE Status__c = 'Processed'];
            System.assertEquals(100, processedOrders.size());
        }
    }

    Always use Test.startTest() and Test.stopTest() in your test methods. These methods reset governor limits for the code executed between them and force any asynchronous processes to complete. This ensures that your tests accurately verify the behavior of your code under normal governor limits.

    // Anti-pattern: Making methods public just for testing
    public class OrderService {
        // Method made public only for testing
        public Boolean isValidOrder(Order__c order) {
            // Validation logic
            return order.Amount__c > 0 && order.Status__c == 'Pending';
        }
        
        public void processOrders() {
            // Process orders using isValidOrder
        }
    }
    
    // Better approach: Use @TestVisible
    public class OrderService {
        // Private method with @TestVisible annotation
        @TestVisible
        private Boolean isValidOrder(Order__c order) {
            // Validation logic
            return order.Amount__c > 0 && order.Status__c == 'Pending';
        }
        
        public void processOrders() {
            // Process orders using isValidOrder
        }
    }

    Don’t make methods or variables public just for testing. This exposes implementation details and breaks encapsulation. Instead, use the @TestVisible annotation to make private members accessible to test classes while keeping them private to the rest of your code.

    // Anti-pattern: Using string literals for field and object names
    public List<SObject> getRecords(String objectName, String fieldName, String value) {
        // Vulnerable to SOQL injection
        String query = 'SELECT Id, Name FROM ' + objectName + 
                       ' WHERE ' + fieldName + ' = \'' + value + '\'';
        return Database.query(query);
    }
    
    // Better approach: Use SObjectType for dynamic references
    public List<SObject> getRecords(String objectName, String fieldName, String value) {
        // Validate object name
        Schema.SObjectType objType = Schema.getGlobalDescribe().get(objectName);
        if (objType == null) {
            throw new IllegalArgumentException('Invalid object name: ' + objectName);
        }
        
        // Validate field name
        Schema.DescribeSObjectResult objDescribe = objType.getDescribe();
        Map<String, Schema.SObjectField> fieldMap = objDescribe.fields.getMap();
        if (!fieldMap.containsKey(fieldName)) {
            throw new IllegalArgumentException('Invalid field name: ' + fieldName);
        }
        
        // Build and execute query safely
        String safeObjectName = String.valueOf(objType);
        String safeFieldName = String.valueOf(fieldMap.get(fieldName));
        
        String query = 'SELECT Id, Name FROM ' + safeObjectName + 
                       ' WHERE ' + safeFieldName + ' = :value';
        return Database.query(query);
    }

    Use Schema.SObjectType and Schema.SObjectField for dynamic references to objects and fields instead of string literals. This prevents SOQL injection vulnerabilities and catches errors at compile time rather than runtime.

    // Anti-pattern: Not enforcing object and field level security
    public List<Account> getAccounts() {
        return [SELECT Id, Name, AnnualRevenue FROM Account];
    }
    
    // Better approach: Use WITH SECURITY_ENFORCED
    public List<Account> getAccounts() {
        return [SELECT Id, Name, AnnualRevenue 
                FROM Account 
                WITH SECURITY_ENFORCED];
    }
    
    // Alternative approach: Use Security.stripInaccessible
    public List<Account> getAccounts() {
        List<Account> accounts = [SELECT Id, Name, AnnualRevenue FROM Account];
        
        // Strip fields the user doesn't have access to
        SObjectAccessDecision decision = Security.stripInaccessible(
            AccessType.READABLE, accounts
        );
        
        return (List<Account>) decision.getRecords();
    }

    Enforce object and field level security in your SOQL queries using WITH SECURITY_ENFORCED or Security.stripInaccessible(). This ensures that users can only access data they have permission to see, following the principle of least privilege.

    // Anti-pattern: Not specifying sharing settings
    public class AccountService {
        public List<Account> getAccounts() {
            return [SELECT Id, Name FROM Account];
        }
    }
    
    // Better approach: Explicitly declare sharing model
    // With sharing: Respects user's permissions
    public with sharing class AccountService {
        public List<Account> getAccounts() {
            return [SELECT Id, Name FROM Account];
        }
    }
    
    // Without sharing: Ignores user's permissions (use carefully)
    public without sharing class SystemService {
        public void performSystemOperation() {
            // Operations that need system-level access
        }
    }
    
    // Inherited sharing: Inherits sharing setting from calling code
    public inherited sharing class UtilityService {
        public List<Account> getAccountsWithSharing() {
            return [SELECT Id, Name FROM Account];
        }
    }

    Always explicitly declare the sharing model for your Apex classes using with sharing, without sharing, or inherited sharing. The with sharing keyword enforces record-level security, while without sharing bypasses it. Use without sharing only when absolutely necessary, and document why it’s needed.

    // Anti-pattern: Monolithic trigger with business logic
    trigger AccountTrigger on Account (before insert, before update, after insert, after update) {
        if (Trigger.isBefore && Trigger.isInsert) {
            // Validation logic
            for (Account acc : Trigger.new) {
                if (acc.Name == null || acc.Name.length() < 3) {
                    acc.Name.addError('Account name must be at least 3 characters');
                }
            }
        } else if (Trigger.isAfter && Trigger.isInsert) {
            // Create related records
            List<Contact> contactsToInsert = new List<Contact>();
            for (Account acc : Trigger.new) {
                Contact con = new Contact(
                    LastName = 'Primary Contact',
                    AccountId = acc.Id
                );
                contactsToInsert.add(con);
            }
            insert contactsToInsert;
        }
        // More logic for other contexts...
    }
    
    // Better approach: Use a trigger framework
    // Trigger - thin dispatcher
    trigger AccountTrigger on Account (before insert, before update, after insert, after update) {
        TriggerHandler.run(new AccountTriggerHandler());
    }
    
    // Handler class
    public class AccountTriggerHandler extends TriggerHandler {
        public override void beforeInsert() {
            AccountService.validateAccounts(Trigger.new);
        }
        
        public override void afterInsert() {
            AccountService.createPrimaryContacts(Trigger.new);
        }
        
        // Other handler methods...
    }
    
    // Service class with business logic
    public class AccountService {
        public static void validateAccounts(List<Account> accounts) {
            for (Account acc : accounts) {
                if (acc.Name == null || acc.Name.length() < 3) {
                    acc.Name.addError('Account name must be at least 3 characters');
                }
            }
        }
        
        public static void createPrimaryContacts(List<Account> accounts) {
            List<Contact> contactsToInsert = new List<Contact>();
            for (Account acc : accounts) {
                Contact con = new Contact(
                    LastName = 'Primary Contact',
                    AccountId = acc.Id
                );
                contactsToInsert.add(con);
            }
            insert contactsToInsert;
        }
    }

    Use a trigger framework to organize your trigger logic. A good framework separates trigger dispatching from business logic, prevents recursive trigger execution, and makes it easier to maintain and test your code. Avoid putting business logic directly in trigger files.

    // Anti-pattern: Poor naming conventions
    public class cls {
        public void p(List<Account> l) {
            for (Account a : l) {
                a.n = a.n.toUpperCase();
            }
            update l;
        }
    }
    
    // Better approach: Use descriptive names
    public class AccountService {
        public void processAccounts(List<Account> accounts) {
            for (Account account : accounts) {
                account.Name = account.Name.toUpperCase();
            }
            update accounts;
        }
    }

    Follow proper naming conventions for classes, methods, and variables. Use descriptive names that clearly indicate the purpose of each entity. This makes your code more readable and maintainable. Class names should be nouns, method names should be verbs, and variable names should be descriptive.

    // Anti-pattern: Doing too much in synchronous context
    public void processLargeDataSet() {
        List<Account> accounts = [SELECT Id FROM Account LIMIT 50000];
        for (Account acc : accounts) {
            // Complex processing
        }
        // More operations that could hit governor limits
    }
    
    // Better approach: Use asynchronous Apex
    public void processLargeDataSet() {
        // Queue the job for asynchronous processing
        System.enqueueJob(new AccountProcessingJob());
    }
    
    // Queueable implementation
    public class AccountProcessingJob implements Queueable {
        public void execute(QueueableContext context) {
            // Process accounts in chunks
            List<Account> accounts = [SELECT Id FROM Account LIMIT 1000];
            
            // Process this batch
            for (Account acc : accounts) {
                // Complex processing
            }
            
            // If more accounts to process, chain another job
            List<Account> remainingAccounts = [SELECT Id FROM Account 
                                             WHERE Id > :accounts[accounts.size()-1].Id 
                                             LIMIT 1];
            if (!remainingAccounts.isEmpty()) {
                System.enqueueJob(new AccountProcessingJob());
            }
        }
    }

    Use asynchronous Apex (Queueable, Batch, Scheduled, or Future methods) for operations that process large amounts of data or might exceed governor limits. Asynchronous Apex has higher governor limits and can be chained or scheduled to process data in manageable chunks.

    ABAPApache Camel
    websitexgithublinkedin
    Powered by Mintlify
    Assistant
    Responses are generated using AI and may contain mistakes.