Skip to main content
  1. Salesforce.com/

Salesforce Apex for Programmers

·26 mins

This is Salesforce Apex introduction for programmers. It can also serve as a quick reference that no one ever seems to need.

Why? #

Because I was tired of seeing the same “what is a constant and variable” type posts that goes through everything from defining how beautiful SFDC is, how Apex can make your dreams come true, and finally get to expanding “OOP”. So, I decided to do something sensible - create yet another “what’s a constant” type post.

Also, because it can become tiring to click through Trailhead and sit through hours of videos that describe how beautiful SFDC is, and blah.

This post is mostly pointers to practical information and examples for Apex beginners who are not conversant in other programming languages.

How to get started? #

  1. Create an org if you don’t already have one
  2. Download VSCode, install Salesforce Extensions
  3. Install SFDX
  4. Create a project and connect to Salesforce

You will find it convenient to use the below mechanism to test/execute code-

  1. Create files in script folder, write code, select code, and hit SFDX: Execute Anonymous Apex with Editor Contents in VSCode command pallette
  2. Create Apex class with SFDX: Create Apex Class and execute that Apex class with MyClass.foo() similar to point (1)

Where to Apex? #

Apex is an object oriented, strongly typed programming language provided by Salesforce. Apex enables us to customize our org by writing validation rules, automation routines (‘update this when I update that’), call external services, or expose custom logic in salesforce to external world. It is a key player in the “controller” layer of the MVC representation of salesforce architecture. Visualforce and Lightning UI invoke Apex to execute backend business logic and carry out data operations.

You can write Apex in three places in salesforce application -

  • Triggers
  • Classes
  • Anonymous

Triggers #

Perform custom actions before or after data operations. Triggers are database-equivalents of “do this before or after some operation happens”. The “operation” can be of -

  • before or after Insert, Update, Upsert, Delete operations
  • after an Undelete operation

Here’s a sample trigger -

trigger beforeAccountCreateHello on Account (before update) {
  System.debug("hello");
}

With this simple trigger, you instruct salesforce to execute trigger beforeAccountCreateHello code before any update operation on Account object.

Triggers can be -

  • before: Validate or update field values before record is saved to database
  • after: Trigger any other operations after the record is saved to database. The current record will be read-only at this stage

You can write as many triggers as you like on an object, but it is recommended to follow a “one trigger per object” pattern.

Also see -

Classes #

Classes are reusable blocks of code which can be instantiated and used in triggers, other classes, Lightning Web Components (LWCs), and other such components in salesforce. Classes are also called user data types, which can be used to create objects.

Here’s a sample class -

public Class SayHello {
    public void hello(String msg) {
       System.debug(msg);
   }
}

Execute the class method with the below code -

SayHello say = new SayHello();
say.hello('hello world');

We are just creating an object say from the class and executing the method.

Classes can be -

  • public: A public access modifier will make the class available to other classes, triggers, etc.
  • private: Available only for other local classes. Typically used with inner classes or for test classes
  • global: Class can be accessed from any Apex. Typically used with webservices

In addition:

  • with sharing or without sharing keywords specify the “sharing mode” - whether the class runs under the sharing rules of current user
  • classes can also be virtual or abstract - your beloved OOP principles apply
  • class can implement an interface

Classes can have public / private / virtual / etc. member variables and member functions. Classes can also have static variables and methods (as opposed to the “normal”(?) instance variables and methods).

You can also have a static method -

public Class SayHello {
    public static void hi(String msg) {
        System.debug(msg);
    }
}

.. in which case you do not instantiate the object (but directly use the class method).

SayHello.hi();

Static variables/ methods are available once the class is loaded and there is a single copy available to all instances of the class. Instance variables (& methods) are created new for every instance of the class.

Also see -

Anonymous Apex #

You can also write quick code for testing/debugging. We do that either in-

  1. Script files as described in previous section
  2. Click the Setup icon in your Salesforce org > Developer Console > Click on Debug in the popup window > Choose Open Execute Anonymous Window

All the Basics #

No ‘here be dragons` here - everything’s cool and in the right place.

Debugging #

  1. Include System.debug('I am here) message in Apex code
  2. Execute Apex
  3. Go through logs to locate the statement and see what goes before/after that statement. In VSCode you will see this in Output panel (just make sure Salesforce CLI is selected)

apex-code-output-vscode

Data Types #

Apex has types and is similar to Java. I absolutely love discussion on the below types supported by Apex -

  • Blob
  • Boolean
  • Date
  • Datetime
  • Decimal
  • Double
  • ID - 18 character representation of record Id. E.g ID id='00300000003T2PGAA0';
  • Integer
  • Long
  • Object
  • String
  • Time

You define / initiate a variable like so -

  • Integer a;
  • Integer a = 1;
Integer a = 1;
Integer b;
b = 2;
Integer sum = a + b;
System.debug('sum: ' + sum);

Mix types if you can use some excitement in your life.

Control Statements #

There are multiple ways to control flow - no surprise there.

  • if/else

     Boolean talk = true;
     if (talk) {
       System.debug('yes');
     }
     else {
       System.debug('no');
     }
    
  • switch

     Integer ct = 1;
     switch on ct {
       when 0 {
        System.debug('zero');
       }
       when 1 {
        System.debug('one');
       }
       when else {
        System.debug('other');
       }
     }
    
  • while

     Integer ct = 0;
     while (ct < 3) {
       System.debug('hello ' + ct);
       ct = ct + 1 ;
     }
    
  • do/while

      Integer ct = 0;
      do {
        System.debug('hello ' + ct);
        ct = ct + 1 ;
      } while (ct < 3);
    
  • for

     for (Integer ct = 0; ct < 3; ct++) {
       System.debug('hello ' + ct);
     }
    

Operators #

  • Arithmetic: +, -, /, *
  • Comparison: =, >, <. Mix comparison operators for max effect - >=, !=, <= etc.
  • Logical: &&, ||
  • Unary: any arithmetic / logical operators, ! not operator
  • Bitwise operators
  • Ternary operator

See operators in Apex for more.

Collections #

There are three types of collections - list, set and maps.

List #

  • ordered
  • allows duplicates
// initiate
List<String> employees = new List<String>();

// add values
employees.add('Siva');
employees.add('John');

System.debug('employees: ' + employees);
// (Siva, John)

// initialize list and assign values
List<Integer> num = new List<Integer>{1,2,3};
System.debug('num: ' + num);
// (1, 2, 3)
System.debug('num [0]: ' + num.get(0));
// 1

// iterate through list - `for`
for (Integer i = 0; i < num.size(); i++) {
    System.debug('i is : ' + i);
}

// iterate through list - `for` alternative
List<Integer> num = new List<Integer>{1,2,3};
for (Integer i:num) {
    System.debug('i am : ' + i);
}

Since SObject (or Salesforce object) is a valid type in Apex, we can do -

List<Contact> contactList = new List<Contact>();

We also have a short notation -

String[] names = new String{'Siva', 'Rama'};

List can go 4 levels deep - List<List<List<List<String>>>>.

See all list methods.

Set #

  • order not important
  • element is unique
  • faster to retrieve elements
Set<String> names = new Set<String>();
names.add('Siva');
names.add('Rama');
System.debug(names);
// {Rama, Siva}

// initialize + add
Set<Integer> nums = new Set<Integer>{1,2,3};
System.debug('nums: ' + nums);
System.debug('Is 1 present? ' + nums.contains(1));
// true

See all set methods.

Map #

  • key-value pairs
  • key is unique
Map<Integer, String> names = new Map<Integer, String>();
names.put(1, 'Siva');
names.put(2, 'Rama');

System.debug('names: ' + names);
// {1=Siva, 2=Rama}

System.debug('name 1: ' + names.get(1));
// Siva

You can mix and match collections too.

Map<Integer, List<String>> names = new Map<Integer, List<String>>();
names.put(1, new List<String>{'Siva', 'John'});
names.put(2, new List<String>{'Jane', 'Mat'});

System.debug('names: ' + names);
// {1=(Siva, John), 2=(Jane, Mat)}

See all map methods.

All collections are limited by only your heap size limits.

Error Handling #

Every problem can be solved by try/catch -

try {
    System.debug('this never fails.');
}
catch (Exception e) {
    // Handle this exception here
}

Built in exceptions-

  • DmlException
  • ListException
  • NullPointerException
  • QueryException
  • SObjectException

Limits #

Apex coding and discussion is not complete without “limits”.

Salesforce has a beautiful, scaleable system - but that system is built on a shared infrastructure. The most effective way to ensure that the infrastructure is available for everyone is to make sure every org on the system uses resources sensibly - ergo, limits. Limits are hard, or soft, constraints on what you can execute, how many resources can be dedicated to execution of the transaction, and how often can you execute stuff.

Salesforce enforces limits for Apex creation and execution on a per transaction and time-limited basis.

Key Limits: Per Transaction #

DescriptionSync LimitAsync Limit
(same as sync if blank)
Total SOQL queries issued100200
Total records retrieved by SOQL queries50,000
Total SOSL queries issues20
# DML statements issues150
# Callouts100
# Future calls50
Heap size6 MB12 MB
CPU time10 seconds60 seconds
Max execution time for each Apex transaction10 min

Key Limits: Overall #

DescriptionLimit
# async Apex method executions (greater of the two)250K/24 hours
(or) number_licenses * 200
Max. Apex class scheduled concurrently100
Batch Apex jobs queued or active concurrently5
Maximum size of callout request or response6 MB (sync) / 12 MB (async)
For loop list batch size200
Max size of Apex class or trigger1 million characters
Max size of code used by all Apex code in org6 MB
# push notifications for mobile app50K (SFDC app)
35K (Custom)
5K (AppExchange)

See more limits on Apex Governor Limits documentation.

Working with Data #

You can perform data operations in Apex using SOQL and SOSL.

SOQL #

Salesforce Object Query Language is similar to SQL, but acts on the Salesforce data model. You can use SOQL in Apex, or directly fire queries using the developer console or sfdx.

Here’s some Apex code using a simple SOQL -

List<Account> acctList = [SELECT name from Account LIMIT 10];
System.debug('account list: ' + acctList);
// (Account:{Name=Acme Inc, Id=001o0000006wAaJAAU}, Account:{Name=GenePoint, Id=001o0000006wA0vAAE}, ...)

You have to use the API name of the field in SOQL. You can find that in Setup > Object Manager > Select the object of interest > Click on Fields and Relationships.

You can also use relationships (implicitly - since these are already defined in object relationships against look-up fields et. al.) -

List<Contact> conList = [
  SELECT Name, Email, Account.Name, Account.AnnualRevenue
  FROM Contact
  WHERE
	Name LIKE 'A%'
  LIMIT 10
  ];

for (Contact con: conList) {
    System.debug(con.Name + '; ' + con.Email + '; ' +  con.Account.Name + '; ' + con.Account.AnnualRevenue);
}

The Account value is the relationship name in Account look up field in Contact.

While the above query works for selecting the parent Account with the child Contact record, we need to build a slightly different query for selecting child records along with parent records -

List<Account> accList = ;

for (Account a: [SELECT Name, AnnualRevenue, (SELECT Id, Name, CloseDate from Opportunities)
      FROM Account
      WHERE
      Name LIKE 'A%'
      LIMIT 10
      ]) {
    System.debug('acct: ' + a.Name + '; ' + a.Opportunities);
}

This way of using the list directly in the for query is preferred for better performance.

You can perform aggregates in SOQL -

List<AggregateResult> aggr = [
  SELECT Type, SUM(AnnualRevenue)
  FROM Account
  WHERE
	Rating != 'Cold'
  GROUP BY Type
  LIMIT 10
  ];

for (AggregateResult r: aggr) {
  System.debug(r);
}

Following operators are valid in SOQL:

  1. =, >, >=, <, <=, != - do exactly as they appear on the tin
  2. INCLUDES, EXCLUDES - Specify WHERE criteria for multi-select picklists
  3. LIKE - Use wildcards
  4. IN, NOT IN

You can do aggregate of SUM, COUNT, AVERAGE, MIN, MAX.

SOSL #

SOSL is another way to access data from Salesforce.

  • perform search on multiple objects in one go
  • works only on text, email and phone fields

Here’s a simple SOSL statement used in Apex -

List<List<SObject>> results = [FIND 'Acme*' in ALL FIELDS RETURNING Account(Name), Contact];

Account[] accList = results[0];
Contact[] conList = results[1];

System.debug('accList: ' + accList);
System.debug('conList: ' + conList);

DML Operations #

SOQL and SOSL help you retrieve results from database. Apex has handy functions to insert, update or delete records.

You can do -

<op> <SObject> List<SObject>

… where -

  • op is either insert, update, upsert, or delete
  • SObject is a valid Salesforce object

You can also do two more operations -

  • merge: Merge upto three records of same SObject
    merge con1 con2
    
  • undelete
    undelete con
    undelete conList
    

Let’s see a few examples.

Start with a simple insert -

Account a = new Account();
a.Name = 'Acme 1 Inc';
insert a;

Or..

Contact con = new Contact(Name='John Doe', Birthdate=Date.valueof('2010-01-01'), AccountId='0011J00001mZTK5QAO');
insert con;

update and delete are equally simple -

Contact con = [SELECT Id FROM Contact WHERE FirstName='John' AND LastName='Doe' AND AccountId='0011J00001mZTK5QAO'];
con.LastName = 'Doe Jr.';
update con;

If you have multiple DML operations in code and any of the DML operations fail, the previous DML operations will be rolled back.

DML Operations: Database #

We have an alternate mechanism to perform DML operations in Apex using Database object.

Instead of direct SObject operations, we do -

  • Database.insert(List<Contact>, true);
  • Database.update(List<Contact>, true);
  • … and so on

The advantage here is the last parameter that signifies allOrNone to allow partial operations.

  • If true, Database operations are very similar to the previous SObject operations.
  • If false, Salesforce commits partial transactions that have been executed before encountering error.

Check out this example -

Account acc1 = new Account(Name='Acme 30 Inc');
Account acc2 = new Account(Name='', AnnualRevenue=1000);

List<Account> acc = new List<Account> {acc1, acc2};
insert acc;

The transaction will fail since Account.Name is required and the second account does not have a name. The first account, though valid, will not be created.

Let’s change the code to use Database operations -

Account acc1 = new Account(Name='Acme 30 Inc');
Account acc2 = new Account(Name='', AnnualRevenue=1000);

List<Account> acc = new List<Account> {acc1, acc2};
Database.SaveResult[] res = Database.insert(acc, false);
System.debug('res: ' + res);

You can now see that the transaction does not throw any error. The res debug output will show that one of the accounts failed due to required field not being present.

Order of Execution #

Salesforce has more than one way to execute business logic. Ergo, it may be useful for you to know what gets executed when.

Here’s a high-level diagram of the execution flow.

salesforce-order-of-execution

Order of execution is not guaranteed across multiple triggers - yet another reason to use a standard trigger pattern and control execution in code.

See order of execution in docs to know more.

Bulk up #

Salesforce can (and will) commit more than a single record in a single transaction. We write Apex to make use of the feature - this is what is referred to as “bulkification of Apex”.

Consider this code that we have seen previously -

Contact con = [SELECT Id FROM Contact WHERE LastName='Anderson' LIMIT 1];
con.Title = 'Neo';
update con;

You can do the update in a loop for multiple records -

Contact[] conList = [SELECT Id FROM Contact WHERE LastName='Anderson'];
for (Contact con: conList) {
  con.Title = 'Neo';
  update con;
}

This is when things get tricky. When you run database transactions like the above example in a loop, it works, but has to perform commits for every Anderson contact record out there. Salesforce limits the no. of commits, no. of SOQL queries, CPU time etc. allowed in a single transaction - we’ve touched on this topic before and too many records may fail the execution.

So, it is a good idea to treat all transactions as bulk transactions. The above code can be easily re-written as -

Contact[] conList = [SELECT Id FROM Contact WHERE LastName='Anderson'];
for (Contact con:ConList) {
con.Title = 'Neo 1';
}

update conList;

Unit Tests #

Salesforce provides a unit testing framework for your Apex code. It is mandatory to have unit tests that cover 75% of Apex code. Unit tests must run successfully for (almost) every deployment. Unit tests are also executed at the time of release upgrades.

Test classes do not count against the 6 MB Apex code limit.

Let’s see an example. Consider this simple Apex class.

public class TemperatureConverter {
    // Takes a Fahrenheit temperature and returns the Celsius equivalent.
    public static Decimal FahrenheitToCelsius(Decimal fh) {
        Decimal cs = (fh - 32) * 5/9;
        return cs.setScale(2);
    }
}

We write test class for the above Apex and cover all run paths -

@isTest
private class TemperatureConverterTest {
    @isTest static void testWarmTemp() {
        Decimal celsius = TemperatureConverter.FahrenheitToCelsius(70);
        System.assertEquals(21.11,celsius);
    }

    @isTest static void testFreezingPoint() {
        Decimal celsius = TemperatureConverter.FahrenheitToCelsius(32);
        System.assertEquals(0,celsius);
    }
    @isTest static void testBoilingPoint() {
        Decimal celsius = TemperatureConverter.FahrenheitToCelsius(212);
        System.assertEquals(100,celsius,'Boiling point temperature is not expected.');
    }

    @isTest static void testNegativeTemp() {
        Decimal celsius = TemperatureConverter.FahrenheitToCelsius(-10);
        System.assertEquals(-23.33,celsius);
    }

}

Run tests in VSCode - Sidebar > Test > Select Apex Tests to run. Or - Ctrl+Shift+P > Run Apex Tests.

execute-apex-tests-vscode

Alternatively, run tests manually in the Developer Console.

execute-apex-tests

Here’s another Apex test example for a trigger.

trigger AccountDeletion on Account (before delete) {

    // Prevent the deletion of accounts if they have related contacts.
    for (Account a : [SELECT Id FROM Account
                     WHERE Id IN (SELECT AccountId FROM Opportunity) AND
                     Id IN :Trigger.old]) {
        Trigger.oldMap.get(a.Id).addError(
            'Cannot delete account with related opportunities.');
    }

}

Test class for the trigger is simple enough -

@isTest
private class TestAccountDeletion {
    @isTest
    static void TestDeleteAccountWithOneOpportunity() {
        // Test data setup
        // Create an account with an opportunity, and then try to delete it
        Account acct = new Account(Name='Test Account');
        insert acct;
        Opportunity opp = new Opportunity(Name=acct.Name + ' Opportunity',
                                       StageName='Prospecting',
                                       CloseDate=System.today().addMonths(1),
                                       AccountId=acct.Id);
        insert opp;

        // Perform test
        Test.startTest();
        Database.DeleteResult result = Database.delete(acct, false);
        Test.stopTest();
        // Verify
        // In this case the deletion should have been stopped by the trigger,
        // so verify that we got back an error.
        System.assert(!result.isSuccess());
        System.assert(result.getErrors().size() > 0);
        System.assertEquals('Cannot delete account with related opportunities.',
                             result.getErrors()[0].getMessage());
    }
}

You may note that -

  • We are creating test data for our tests. This data is not persisted
  • If you want to live life on the edge (or absolutely need your tests to “see” org data), you should use @isTest(SeeAllData=true) annotation against your test methods

See Apex testing docs to know more.

Revisiting Triggers #

Let us deep-dive into a couple of concepts now that we know better.

Collections in Triggers #

Triggers provide useful context variables to validate or update field values.

This may range from variables like isInsert, isBefore that specify the operation type, to collections like newMap, old (list) that are automatically populated with the new or old values being inserted, updated or deleted.

We can do this to set a default Rating of Warm for all accounts -

trigger AccountTrigger on Account (before insert, after insert, before update, after update) {

    if (trigger.isInsert) {
        System.debug('Inserting record');
        if (trigger.isBefore) {
            System.debug('Before insert');
            for (Account a: Trigger.new) {
                a.Rating = 'Warm';
            }
        }
    }

    else if (trigger.isUpdate) {
        System.debug('Updating record');
        if (trigger.isAfter) {}
        else {
            System.debug('before update: ' + Trigger.oldMap);
        }
    }
}

Collections available in triggers are below.

ValueDescriptionCan be used in
newList of new values of SObject recordsinsert, update, undelete triggers; can be modified only in before trigger
newMapReturns a map - <Id, SObject> of new values of SObject recordsafter insert, before/after update, after undelete triggers
oldList of old values of SObjectupdate, delete
oldMapMap of old values of <Id, SObject>update, delete

See all context variables available to triggers.

Bulkify Triggers #

Trigger code, like the rest of Apex, should be “bulkified” as well.

Let’s consider an example. We have to set Contact’s Title to Contact’s Account’s Rating. (well, what can I say - scenarios can be wierd like that).

Normally, your will write some trigger code -

trigger ContactTitleFromRating on Contact (before insert) {

    for (Contact con : Trigger.new) {
        if (con.AccountId != null)
            con.Title = [Select Rating from Account Where Id = :con.AccountId].Rating;
    }
}

While this works and is perfectly acceptable for single records, remember that we can always deal with more than one record in Apex. The above code will fire off select rating query for every record being updated. This is the quickest way to the much loved “too many SOQLs” error - thanks to the Salesforce limits.

We can rewrite the code to something more sane -

trigger ContactTitleFromRating on Contact (before insert) {
    Set<Id> accountIds = new Set<Id>();

    // get all account ids from trigger
    // .. add them to a set (to filter out duplicates)
    for (Contact con : Trigger.new)
        accountIds.add(con.AccountId);

    // query accounts for Rating
    Map<id, Account> accList = new Map<id, Account>([Select Rating from Account Where Id in :accountIds]);

    // re-iterate trigger.new
    // fetch Rating from the set and populate title
    for (Contact con : Trigger.new) {
        if (con.AccountId != null)
            con.Title = accList.get(con.AccountId).Rating;
    }
    // the actual update happens due to trigger functionality
}

Trigger Patterns #

One moment you know what triggers are capable of, and the next moment you are writing code like this -

// example similar to that provided by salesforce.com
trigger myAccountTrigger on Account(before delete, before insert, before update,
    after delete, after insert, after update) {
    if (Trigger.isBefore) {
        if (Trigger.isDelete) {
            // In a before delete trigger, the trigger accesses the records that will be
            // deleted with the Trigger.old list.
            for (Account a: Trigger.old) {
                if (a.name != 'okToDelete') {
                    a.addError('You can\'t delete this record!');
                }
            }
        } else {
            // In before insert or before update triggers, the trigger accesses the new records
            // with the Trigger.new list.
            for (Account a: Trigger.new) {
                if (a.name == 'bad') {
                    a.name.addError('Bad name');
                    }
                }
                if (Trigger.isInsert) {
                    for (Account a: Trigger.new) {
                        System.assertEquals('xxx', a.accountNumber);
                        System.assertEquals('industry', a.industry);
                        System.assertEquals(100, a.numberofemployees);
                        System.assertEquals(100.0, a.annualrevenue);
                        a.accountNumber = 'yyy';
                    }
                }
            }
        }

    } else {
        // If the trigger is not a before trigger, it must be an after trigger.
        if (Trigger.isInsert) {
            List < Contact > contacts = new List <Contact> ();
            for (Account a: Trigger.new) {
                if (a.Name == 'makeContact') {
                    contacts.add(new Contact(LastName = a.Name,
                        AccountId = a.Id));
                }
                3
                insert contacts;
            }
        }
    }
}

While this is doing a lot of good, there are ways to make it better. Enter trigger patterns.

There is no “one official trigger pattern” to follow. What we see below is an amalgamation of best practices -

  • encourage a modular approach to writing triggers and associated logic
  • write reusable code
  • more maintainable code that your support guy never complains about

A simple approach was outlined by salesforce in one of the developer blog posts, and I can get behind it -

  • create an interface ITrigger that can be used across our salesforce instance. This will provide access to all possible trigger methods
  • create a reusable class called TriggerFactory that gets called from all triggers. This is called by ITrigger
  • create a trigger handler class for individual objects that will implement our ITrigger interface. For e.g. AccountHandler, ContactHandler.
  • finally, write minimal code in the trigger itself - see example AccountTrigger below. This will be the only trigger defined for the object

Let’s see the code.

ITrigger Interface #

Here’s the interface -

public interface ITrigger {
    /**
     * bulkBefore
     *
     * This method is called prior to execution of a BEFORE trigger. Use this to cache
     * any data required into maps prior execution of the trigger.
     */

    void bulkBefore();

    /**
     * bulkAfter
     *
     * This method is called prior to execution of an AFTER trigger. Use this to cache
     * any data required into maps prior execution of the trigger.
     */

    void bulkAfter();

    /**
     * beforeInsert
     * This method is called iteratively for each record to be inserted during a BEFORE
     * trigger. Never execute any SOQL/SOSL etc in this and other iterative methods.
     */

    void beforeInsert(SObject so);

    /**
     * beforeUpdate
     *
     * This method is called iteratively for each record to be updated during a BEFORE
     * trigger.
     */

    void beforeUpdate(SObject oldSo, SObject so);

    /**
     * beforeDelete
     *
     */

    void beforeDelete(SObject so);

    // More code to implement everything incl. kitchensink
}

Trigger Factory #

Common resuable code that gets called by all triggers.

{
    // example by salesforce
    /**
     * Class TriggerFactory
     * Used to instantiate and execute Trigger Handlers associated with sObjects.
     */
    public with sharing class Triggerfactory {
        /**
         * Public static method to create and execute a trigger handler
         * Arguments: Schema. sObjectType soType - Object type to process (SObject.sObjectType)
         * Throws a TriggerException if no handler has been coded.
         */
        public static void createHandler(Schema.sObjectType soType) {
            // Get a handler appropriate to the object being processed
            ITrigger handler = getHandler(soType);
            // Make sure we have a handler registered, new handlers must be registered in the getHandler method.
            if (handler == null) {
                throw new TriggerException('No Trigger Handler registered for Object Type: ' + soType);
            }
            // Execute the handler to fulfill the trigger
            execute(handler);
        }

        /**
         * private static method to control the execution of the handier
         * Arguments: ITrigger handler - A Trigger Handler to execute
         */
        private static void execute(ITrigger handler) {
            // Before Trigger
            if (Trigger.isBefore) {
                // Call the bulk before to handle any caching of data and enable bulkification
                handler.bulkBefore();
                // ..rest of code
            }
            // ..rest of operations/actions
        }
    }

Trigger Handler #

Write trigger handler classes that are called by actual trigger code against objects.

public with sharing class AccountHandler
implements ITrigger {

    // Constructor
    public AccountHandler() {}

    /**
    * bulkBefore
    =
    * This method is called prior to execution of a BEFORE trigger. Use this to cache
    * any data required into maps prior execution of the trigger.
    */
    public void bulkBefore() {
        // If this a delete trigger, cache a list of Account Ids that are 'in use'
    }
    public void bulkAfter() {
    }

    public void beforeInsert(SObject so) {
    // code code code
    // code the trigger
    }
}

Object Trigger #

Finally, the minimal code in trigger itself to call the trigger handler class.

trigger AccountTrigger on Account (after delete, after insert, after update){
    TriggerFactory.createHandler(Account, sObjectType)
}

By using a trigger pattern, you can -

  1. Control order of execution of code
  2. Write classes to handle business logic and reuse them in triggers or other classes
  3. Easier control of recursion - just use flags to control which code can be executed recursively

Asynchronous Apex #

So far we have seen how Apex works -

  1. You call Apex through triggers, flow or wherever
  2. Apex executes

This happens in real-time and the transaction waits for Apex to complete. But, there are many scenarios where transactions are required to be processed in the background, and not make the user wait until they complete. For e.g. -

  1. An integration call to send SFDC updates to external system
  2. Automation that does not need user intervention. For e.g. create a feedback activity when a service request is closed

We can write code in Apex to do stuff asynchronously. Async processing also has more generous limits as compared to their sync counterparts (e.g. timeout is 12s instead of 6s in sync mode). There are other advantages too -

  1. Run pseudo parallel jobs. Run tasks asynchronously while the main thread is busy on some other tasks
  2. Mixed DML operations. Apex does not support both user data and setup data operations in the same transaction. Using async Apex you can hand-over one of those operations to another thread
  3. Long running tasks are best run async. For e.g. process all contacts that turned inactive.
  4. Previously discussed scenario of integration call-out

There is more than one way of writing async Apex.

Future Method #

The easiest of them all is future apex. Just annotate the method that needs to run async with @future.

Create a class -

public Class SayHello {
    @future
    public static void hinext(String msg) {
        System.debug(msg);
    }
}

Execute this anonymous Apex -

System.debug('start..');
SayHello.hinext('hello world');
System.debug('end.');

If you executed the Apex in VSCode, you will see two debug statements -

start..
end.

You can see that the future method is invoked from the limits summary in log, but do not see the debug statement. If you execute Apex in Developer Console, you will see only the future method output -

hello world

In fact, there are two logs - one for the anon Apex execution and one for the future handler. Also, you can see the future method execution task entry in Setup > Apex > Apex Jobs.

The two common use cases for future method are -

  1. Mixed DML operations
  2. Call out

Let’s see a practical example. We will write Apex to -

  1. Create user, assign to profile
  2. Create a contact

Create a class CreateUserContact -

public with sharing class CreateUserContact {
  public void CreateUserContactRec(String email, String lastname, String alias) {

    Profile p = [SELECT Id FROM Profile WHERE Name='Standard User'];
    UserRole r = [SELECT Id FROM UserRole WHERE Name='COO'];
    User usr = new User(alias = alias, email=email, lastname=lastname,
            languagelocalekey='en_US', localesidkey='en_US',
            profileid = p.Id, userroleid = r.Id, timezonesidkey='America/Los_Angeles',
            EmailEncodingKey='UTF-8',
            username=email);
    insert usr;

    Contact con = new Contact(FirstName='John', LastName='Doe');
    insert con;

  }
}

Try to run this class with anonymous Apex -

CreateUserContact cuc = new CreateUserContact();
cuc.CreateUserContactRec('joe.user1@example.com', 'User', 'joe1');

You should see an error -

23:38:11.195 (1196433779)|FATAL_ERROR|System.DmlException: Insert failed. First exception on row 0; first error: MIXED_DML_OPERATION, DML operation on setup object is not permitted after you have updated a non-setup object (or vice versa): Contact, original object: User: []

See more on which SObjects cannot be used together.

Let’s use the future method to create user. Modify CreateUserContact -

public with sharing class CreateUserContact {
  public void CreateUserContactRec(String email, String lastname, String alias) {


    CreateUserRec(email, lastname, alias);
    Contact con = new Contact(FirstName='John', LastName='Doe');
    insert con;

  }

  @future
  public static void CreateUserRec(String email, String lastname, String alias) {
    Profile p = [SELECT Id FROM Profile WHERE Name='Standard User'];
    UserRole r = [SELECT Id FROM UserRole WHERE Name='COO'];
    User usr = new User(alias = alias, email=email, lastname=lastname,
            languagelocalekey='en_US', localesidkey='en_US',
            profileid = p.Id, userroleid = r.Id, timezonesidkey='America/Los_Angeles',
            EmailEncodingKey='UTF-8',
            username=email);
    insert usr;
  }
}

Your code should run without problems now.

Note that future methods -

  • should be static
  • allow parameters of primitive or primitive collection types (no SObject or anything)
  • do not guarantee sequence of execution
  • a future method cannot call another future method (no chaining)

Queueable Apex #

Queueable remediates limitations of future method. Using queueable Apex, you can -

  1. Pass any parameter types incl. SObject
  2. Control order of execution
  3. Call other queueable Apex and chain jobs
  4. More easy to monitor since you have a definite job id :)

Implement queueable Apex by creating a new class CreateUserQue -

public class CreateUserQue implements Queueable{
    public final Map<String, String> data;

    public CreateUserQue(Map<String, String> input)
    {
        this.data = input;
    }

    public void execute(QueueableContext ctx) {
        CreateUserRec();
    }
    public void CreateUserRec() {
        Profile p = [SELECT Id FROM Profile WHERE Name='Standard User'];
        UserRole r = [SELECT Id FROM UserRole WHERE Name='COO'];
        User usr = new User(alias = data.get('alias'), email=data.get('email'), lastname=data.get('lastname'),
                languagelocalekey='en_US', localesidkey='en_US',
                profileid = p.Id, userroleid = r.Id, timezonesidkey='America/Los_Angeles',
                EmailEncodingKey='UTF-8',
                username=data.get('email'));
        insert usr;
        System.debug('created user.');
    }
}

You may observe that we created a constructor to receive the variables passed by caller.

We will create a second class for Contact operation -

public class CreateContactQue implements Queueable{
    public final Map<String, String> data;

    public CreateContactQue(Map<String, String> input)
    {
        this.data = input;
    }

    public void execute(QueueableContext ctx) {
        CreateContactRec();
    }

    public void CreateContactRec() {
        Contact con = new Contact(FirstName='John', LastName='Doe');
        insert con;
        System.debug('created contact.');
    }
}

All we need to do now is call the queueable Apex -

Map<string, string> inData= new Map<string, string>{'email'=>'joe.user2@example.com', 'lastname' => 'User', 'alias' => 'joe2'};
CreateUserQue cuq = new CreateUserQue(inData);
Id jobId1 = System.enqueueJob(cuq);
CreateContactQue cuc = new CreateContactQue(inData);
Id jobId2 = System.enqueueJob(cuc);

While the above code works, it also enqueues two seemingly independent jobs. We can make them execute in sequence. Change CreateUserQue to enqueue CreateContactQue -

public class CreateUserQue implements Queueable{
    public final Map<String, String> data;

    public CreateUserQue(Map<String, String> input)
    {
        this.data = input;
    }

    public void execute(QueueableContext ctx) {
        CreateUserRec();
        System.enqueueJob(new CreateContactQue(data));
    }
    public void CreateUserRec() {
        Profile p = [SELECT Id FROM Profile WHERE Name='Standard User'];
        UserRole r = [SELECT Id FROM UserRole WHERE Name='COO'];
        User usr = new User(alias = data.get('alias'), email=data.get('email'), lastname=data.get('lastname'),
                languagelocalekey='en_US', localesidkey='en_US',
                profileid = p.Id, userroleid = r.Id, timezonesidkey='America/Los_Angeles',
                EmailEncodingKey='UTF-8',
                username=data.get('email'));
        insert usr;
        System.debug('created user.');
    }
}

You can now call CreateUserQueue to execute jobs one after the other -

Map<string, string> inData= new Map<string, string>{'email'=>'joe.user2@example.com', 'lastname' => 'User', 'alias' => 'joe2'};
CreateUserQue cuq = new CreateUserQue(inData);
Id jobId1 = System.enqueueJob(cuq);

Batch #

Schedule Apex #

Integration #

See More #

Not satisfied with the above end-to-end, “deep-dive”, “complete guide” on Apex? Do you feel there is much more than simply writing code and saving the world? Fear not - head over to the below links -