Apex and Visualforce Best Practices


  • There should only be one trigger for each object.
Apex Trigger Code

trigger ContactTrigger on Contact (after delete, after insert, after undelete, 
after update, before delete, before insert, before update) 
        ContactTriggerHandler handler = new ContactTriggerHandler(Trigger.isExecuting, Trigger.size);

        if(Trigger.isInsert && Trigger.isBefore)
	else if(Trigger.isInsert && Trigger.isAfter){

	else if(Trigger.isUpdate && Trigger.isBefore){
		handler.OnBeforeUpdate(Trigger.old,, Trigger.newMap);
	else if(Trigger.isUpdate && Trigger.isAfter){
		handler.OnAfterUpdate(Trigger.old,, Trigger.newMap);

	else if(Trigger.isDelete && Trigger.isBefore){
		handler.OnBeforeDelete(Trigger.old, Trigger.oldMap);
	else if(Trigger.isDelete && Trigger.isAfter){
		handler.OnAfterDelete(Trigger.old, Trigger.oldMap);

	else if(Trigger.isUnDelete){

Apex Class Code

public with sharing class ContactTriggerHandler {
	private boolean isHandlerExecuting = false;
	private integer BatchSize = 0;

	public ContactTriggerHandler(boolean isExecuting, integer size){
		isHandlerExecuting = isExecuting;
		BatchSize = size;

	public void OnBeforeInsert(Contact[] newContacts){
		//Example usage
		for(Contact newContact : newContacts){
			if(newContact.Name == null){
				newContact.Name.addError('Missing Name Field');

	public void OnAfterInsert(Contact[] newContacts){


	@future public static void OnAfterInsertAsync(Set newContactIDs){
		//Example usage
		List newContacts = [select Id, Name from Contact where Id IN :newContactIDs];

	public void OnBeforeUpdate(Contact[] oldContact, Contact[] updatedContacts, Map contactMap){
		//Example Map usage
		Map contacts = new Map( [select Id, FirstName, LastName, Email from Contact where Id IN :contactMap.keySet()] );

	public void OnAfterUpdate(Contact[] oldContacts, Contact[] updatedContacts, Map contactMap){


	@future public static void OnAfterUpdateAsync(Set updatedContactIDs){
		List updatedContacts = [select Id, Name from Contact where Id IN :updatedContactIDs];

	public void OnBeforeDelete(Contact[] contactsToDelete, Map contactMap){


	public void OnAfterDelete(Contact[] deletedContacts, Map contactMap){


	@future public static void OnAfterDeleteAsync(Set deletedContactIDs){


	public void OnUndelete(Contact[] restoredContacts){


	public boolean IsTriggerContext{
		get{ return isHandlerExecuting;}

	public boolean IsVisualforcePageContext{
		get{ return !IsTriggerContext;}

	public boolean IsWebServiceContext{
		get{ return !IsTriggerContext;}

	public boolean IsExecuteAnonymousContext{
		get{ return !IsTriggerContext;}

  • Use Helper Classes

Dynamic Delete Batch Apex Class
global class dynamicDelete implements Database.Batchable{

   global final String Query;

   global dynamicDelete(String q)

   global Database.QueryLocator start(Database.BatchableContext BC)
      return Database.getQueryLocator(query);

   global void execute(Database.BatchableContext BC, List scope)
      delete scope;

Scheduler for Batch Apex Delete

global class dynamicDeleteScheduler implements Schedulable{
   private static final String deleteLeads = 'Select id from Lead where Email like \'%test%\' limit 5000';
   private static final String deleteTasks = 'Select id from Task where WhoId IN (Select id from Contact where email like \'%test%\') limit 50000';
   private static final String deleteOpportunities = 'Select id from Opportunity where id in (Select OpportunityId from OpportunityContactRole where Contact.Email like \'%test%\') limit 50000';
   private static final String deleteCases = 'Select id from Case where ContactId IN (Select id from Contact where Email like \'%test%\') limit 50000';
   private static final String deleteTestAccounts = 'Select id from Account where Email__c like \'%test%\' limit 50000';
   private static final String deleteTestContacts = 'Select id from Contact where Email like \'%test%\' limit 50000';

   global void execute(SchedulableContext SC) 
      dynamicDelete deleteTask = new dynamicDelete(deleteTasks);

      dynamicDelete deleteCase = new dynamicDelete(deleteCases);

      dynamicDelete deleteOpportunity = new dynamicDelete(deleteOpportunities);

      dynamicDelete deletecontact = new dynamicDelete(deleteTestContacts);

      dynamicDelete deleteaccount = new dynamicDelete(deleteTestAccounts); 

    Bulkify Triggers to process up to 200 records for each call. This will help you avoid hitting DML limit per process.
Bulk your DML operations with with List/Maps/Sets
private void bulkUpdateExample(Map updateContactOwner)
    List<Contact> listToHoldListOfContactsToBeUpdated = new List<Contact>();
    Id salesForceAdminId = [Select id from User where Name='Salesforce Administrator'].Id;
    for (Id contactIdToUpdateOwner : updateContactOwner.keySet())
          updateContactOwner.get(contactIdToUpdateOwner).OwnerId = salesForceAdminId;
          //Add all records to a List/Map/Set to be updated outside the for loop
    //Always update outside a for loop
    update listToHoldListOfContactsToBeUpdated;

Bulkify your Test classes with at least 200 records

@isTest(SeeAllData=false) static void testContactOwnerReassignmentSuccess()
  Account createAccount = new Account(Name='[TEST]');
  insert createAccount;

  List contactList = new List();
  for (int k = 0; k < 200; k++)
      contactList.add(new Contact(FirstName='[Test]' + k, AccountId=createAccount.Id));

  insert contactList;

  System.assert(200 == [Select count() from Contact where 
Owner.Name = 'Salesforce Administrator']);
    Use Collections in SOQL “WHERE” clauses to retrieve all records using a single query. Also use self, semi and anti joins:
 legalDocumentNames = new String[]
            'Employment Application',
            'Release of Liability',
            'Non-Disclosure Agreement',
            '%Employment Agreement'
List legalDocumentQuery = [select ContactId from Asset
where Product2.Name Like :legalDocumentNames];


    Use @future annotation for methods that can be executed asynchronously
@future public static void OnAfterInsertAsync(Set newAssetIDs)
    new smbNurtureCampaign().insertCampaignMemebersToControlAndTestCampaign(newAssetIDs);
    Provide proper exception handling.
Apply try catch to all methods
public static User getSalesRepsForRouting(List getListActiveUser)
          return getListActiveUser.size() == 1 ? getListActiveUser[0] : getListActiveUser[0];
        catch(handlerException e)
            Debugger.createDebugRecord(e, 'ACRoundRobinV5');
            return null;

Extend the Exception Class

      public class handlerException extends Exception{}

Catch Exception and create record in custom Debugger Object

public static void createDebugRecord(handlerException exceptionObject, String methodName)
         Database.insert(new Debugger__c
                    Message__c = exceptionObject.getMessage(),
                    Line_Number__c = exceptionObject.getLineNumber(),
                    Stack_Trace__c = exceptionObject.getStackTraceString(),
            ), false);
     public static void createDebugRecord(DMLException dmlexceptionObject, String methodName)
         Database.insert(new Debugger__c
                    DML_Record_Id__c = dmlexceptionObject.getDmlId(0),
                    DML_Type__c = dmlexceptionObject.getTypeName(),
                    Message__c = dmlexceptionObject.getDmlMessage(0),
                    Line_Number__c = dmlexceptionObject.getDmlIndex(0),
                    Stack_Trace__c = dmlexceptionObject.getDmlStatusCode(0),
    Prevent SOQL and SOSL injection attacks by using static queries, binding variables or the escapeSingleQuotes method.
        private static final String[] QUERY_SELECT_STATEMENT =
        new String[]{'select ',' from ', ' where Id = \''};

        String query = QUERY_SELECT_STATEMENT[0];
        for(Schema.DescribeFieldResult dfr : queryFields)
            query = query + dfr.getName() + ',';

        query = query.subString(0,query.length() - 1);
        query = query + QUERY_SELECT_STATEMENT[1];
        query = query + obj.getName();
        query = query + QUERY_SELECT_STATEMENT[2];
        query = query + theId + '\'';
    When querying large data sets, use a SOQL “for” loop
        for (Account accountToCheckAndUpdateAccountDetails : [Select BillingStreet, BillingCity, BillingState, BillingPostalCode, BillingCountry, ShippingStreet, ShippingCity, ShippingState, ShippingPostalCode, ShippingCountry, Phone, Fax, Website, Industry from Account])
        update accountToCheckAndUpdateAccountDetails;
    Use Apex Limits Methods to avoid hitting governor exceptions.
In the if statement we check if it is save to proceed without hitting governor limits 
List accountOwnerDetail = [SELECT OwnerId, Owner.Name, Owner.Profile.Name, Owner.isActive from Account];
if (accountOwnerDetail.size() + Limits.getDMLRows() < Limits.getLimitDMLRows())
    No SOQL or SOSL queries inside loops
Create parent-to-child and child-to-parent relationship queries outside for loops
Contact getContactWithAccount = Database.query('Select Name, Email,  Account.Name, Account.BillingStreet from Contact limit 1');
Account getAccountContactOpportunityInOneQuery = Database.query('Select Id, (Select Name from Account.Contacts), (Select Name from Account.Opportunities) from Account limit 1');

Tip: Update an Object without querying for it

List accountFieldsToBeUpdated = new List();
for (Contact updateAccountsFieldFromContact : [Select AccountId from Contact where OtherCity ='San Francisco'])
    //create new account object with the accountId from contact
     Account newAccountObject = new Account(Id=updateAccountsFieldFromContact.AccountId);
     newAccountObject.ShippingCity = 'SF';
     newAccountObject.BillingCity = 'SF';   
update accountFieldsToBeUpdated ;
    Do not use hardcoded IDs: As ID's are unique and different between environments.
Don't use ID's rather get Id's by Name
        Opportunity getOpportunity = [Select Name from Opportunity where Owner.Id='00560000001S1eh' limit 1];

Rather do the following as your unit tests will fail in other environments

        Opportunity getOpportunity = [Select Name from Opportunity where Owner.Name='John Black' limit 1];


    Do not hardcode picklists in Visualforce pages; include them in the controller instead.
Picklist Controller Method
public List getFilterByRecordTypeList() 
            List<SelectOption> op = new List<SelectOption>();
            Set<Id> setPPFamily = new Set<Id>();
            setPPFamily.add('All Opportunities');
            List<AggregateResult> aggregateResultsOfMotivatingProductFamily = [SELECT RecordType.Name FROM Opportunity Group By RecordType.Name];

            for (AggregateResult ppfamilyname : aggregateResultsOfMotivatingProductFamily)
            op.add(new SelectOption((String)ppfamilyname.get('RecordType.Name'),(String)ppfamilyname.get('RecordType.Name')));

            return op;

Calling Picklist Controller in Visualforce

<apex:outputText style="color:#f8f8ff;font-weight:bold" value="Filter By Document Type"/>
<apex:toolbarGroup itemSeparator="line" location="left" id="toobarGroupForm2">
<apex:selectList label="Filter by Record Type" value="{!FilterByRecordType}" size="1" required="false" >
<apex:actionSupport event="onchange" action="{!FilterByRecordTypeAction}" status="filterByRecordType" reRender="innerblock"/>
<apex:outputPanel style="color:#f8f8ff;font-weight:bold">
<apex:actionStatus id="filterByRecordType" startText="Filtering your records..." stopText=""/>
<apex:selectOptions value="{!FilterByRecordTypeList}"/>
    Javascript and CSS should be included as Static Resources allowing the browser to cache them.
Import your JS and CSS to the Static Resources as Zip of single files. Include CSS on top and JS at the bottom for faster loading
<apex:page controller="YourCustomController" sidebar="true" cache="true" showChat="true" >
<apex:stylesheet value="{!URLFOR($Resource.jQuery1, '/css/ui-lightness/jquery-ui-1.8.23.custom.css')}" />

<apex:includeScript value="{!URLFOR($Resource.jQuery1, '/js/jquery-1.8.0.min.js')}" />
<apex:includeScript value="{!URLFOR($Resource.jQuery1, '/js/jquery-ui-1.8.23.custom.min.js')}" />

    Mark controller variables as “transient” if they are not needed between server calls. This will make your page load faster as it reduces the size of the View State.
Transient Example Visualforce Page
&lt;apex:page controller="ExampleController"&gt;
&lt;apex:form id="theForm"&gt;
&lt;apex:panelBar id="thePanel"&gt;
&lt;apex:panelbarItem label="Transient vs Non Transient"&gt;
&lt;b&gt;Transient Time: &lt;/b&gt; {!t1} &lt;br /&gt;
&lt;b&gt;Non Transient Time: &lt;/b&gt; {!t2}
&lt;apex:actionPoller interval="5" enabled="true" rerender="thePanel" action="{!refresh}" /&gt;

Transient Example Controller

public class ExampleController {

    public PageReference refresh() {
        return null;

    DateTime t1;
    transient DateTime t2;

    public String getT1() {
        if (t1 == null) t1 =;
        return '' + t1;

    public String getT2() {
        if (t2 == null) t2 =;
        return '' + t2;
    Use the <repeat> tag to iterate over large collections.
&lt;apex:page standardController="Opportunity" id="thePage"&gt;
&lt;table id="EquipmentTable" border="1"&gt;
&lt;apex:repeat value="{!Opportunity}" var="line" id="theRepeat"&gt;
&lt;td valign="top"&gt;&lt;apex:outputField value="{!line.Id}" /&gt;&lt;/td&gt;
&lt;td valign="top"&gt;&lt;apex:outputField value="{!line.Name}" /&gt;&lt;/td&gt;
&lt;td valign="top"&gt;&lt;apex:outputField value="{!line.Probability}" /&gt;&lt;/td&gt;
&lt;td valign="top"&gt;&lt;apex:outputField value="{!line.Amount}" /&gt;&lt;/td&gt;

Leave a Comment

Fill in your details below or click an icon to log in: Logo

You are commenting using your account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s