The Apex Master Class

Apex For Loop Best Practices (Master Class Ep. 30)

4 min read

Subscribe on YouTube

The Mistakes Everybody Makes Once

Loops are where new Salesforce developers do the most damage, and it’s always the same short list of mistakes. Every one of them is trivially avoidable if you know to avoid it, and every one of them will take down a production transaction if you don’t.

Quick heads up: I’m going to talk about SOQL and DML here, and we haven’t properly covered either yet. SOQL is how you query records, DML is how you insert, update and delete them. That’s enough to follow along. Full episodes on both are coming.


Mistake 1: DML Inside A Loop

This is the big one. This is the one.

Code
//NEVER. DO. THIS.
for (Contact cont : contactsToInsert)
{
    insert cont;
}

Looks perfectly reasonable. Reads like English. Works beautifully when you test it with two contacts.

Then it hits production, someone imports 200 records, and you get:

System.LimitException: Too many DML statements: 151

Salesforce allows 150 DML statements per transaction. That loop uses one per record. At 151 records the whole transaction dies and rolls back. Not the loop. The whole thing.

The fix takes about four seconds:

Code
//ONE DML statement. Works with 1 record or 10,000.
insert contactsToInsert;

And when you need to build the collection up first, do that in the loop and hit the database after it:

Apex
List<Contact> contactsToUpdate = new List<Contact>();

for (Contact cont : contacts)
{
    if (cont.Email == null)
    {
        cont.Email = 'noemail@example.com';
        contactsToUpdate.add(cont);      //collect, don't commit
    }
}

//Outside the loop. Always outside the loop.
if (!contactsToUpdate.isEmpty())
{
    update contactsToUpdate;
}

Note the isEmpty() check. Running DML on an empty list doesn’t error, but it does burn one of your 150 for absolutely no reason. Free win.


Mistake 2: SOQL Inside A Loop

Same disease, different limit. You get 100 SOQL queries per transaction.

Apex
//ALSO NEVER
for (Contact cont : contacts)
{
    Account acct = [SELECT Name FROM Account WHERE Id = :cont.AccountId];
    System.debug(acct.Name);
}

101 contacts and you’re dead: System.LimitException: Too many SOQL queries: 101

The fix is the Set-then-Map pattern from episode 19, and this is exactly why I made such a fuss about it:

Apex
//Gather the Ids first
Set<Id> accountIds = new Set<Id>();
for (Contact cont : contacts)
{
    accountIds.add(cont.AccountId);
}

//ONE query
Map<Id, Account> accountsById = new Map<Id, Account>(
    [SELECT Id, Name FROM Account WHERE Id IN :accountIds]
);

//Now look up from memory. No more queries.
for (Contact cont : contacts)
{
    System.debug(accountsById.get(cont.AccountId)?.Name);
}

Two loops instead of one, and it feels like more code. It is more code. It also works with 10,000 records instead of falling over at 101.


Mistake 3: Nested Loops Over Big Collections

This one won’t throw a limit exception straight away, which somehow makes it worse:

Apex
//200 x 200 = 40,000 iterations
for (Contact cont : contacts)
{
    for (Account acct : accounts)
    {
        if (cont.AccountId == acct.Id)
        {
            System.debug('Match');
        }
    }
}

Every extra record multiplies the work. It’ll seem fine in testing, then quietly eat your 10 second CPU limit in production and throw System.LimitException: Apex CPU time limit exceeded, which is one of the more miserable errors to debug because it doesn’t point at any particular line.

Same fix. A Map turns the inner loop into an instant lookup:

Apex
Map<Id, Account> accountsById = new Map<Id, Account>(accounts);

for (Contact cont : contacts)
{
    if (accountsById.containsKey(cont.AccountId))
    {
        System.debug('Match');
    }
}

40,000 iterations down to 200. If you ever find yourself nesting a loop inside a loop, stop and ask whether a Map would do it.


Mistake 4: Anything Expensive That Doesn’t Change

Apex
//Recalculating the same thing 200 times
for (Contact cont : contacts)
{
    String prefix = Schema.SObjectType.Contact.getKeyPrefix();
    //...
}

//Work it out once
String prefix = Schema.SObjectType.Contact.getKeyPrefix();
for (Contact cont : contacts)
{
    //...
}

If it doesn’t change between iterations, it doesn’t belong in the loop. Simple as that. Describe calls and custom setting lookups are the usual suspects.


The One Rule

If you remember nothing else from this episode, remember this:

Never put SOQL or DML inside a loop.

That single rule is called bulkification, and it’s the thing that separates Apex that survives contact with real data from Apex that doesn’t. It’s also the first thing any decent reviewer looks for, and the first thing a static analysis tool like the SFDX Scanner will flag.

Every experienced Salesforce developer has this burned into their brain, and most of us got it burned in the hard way. Now you don’t have to.


What’s Next

That’s the fundamentals done. Thirty episodes in and you’ve got classes, methods, variables, collections, conditionals and loops. That’s genuinely most of a programming language.

The last three episodes are the big ones, full length deep dives into the three things you’ll use every single day as a Salesforce developer. We start in the next episode with the complete guide to Apex Triggers.

See you next time!


Get Coding With The Force Merch!!

We now have a redbubble store setup so you can buy cool Coding With The Force merchandise! Please check it out! Every purchase goes to supporting the blog and YouTube channel.

Get Shirts Here!
Get Cups, Artwork, Coffee Cups, Bags, Masks and more here!


Check Out More Coding With The Force Stuff!

If you liked this post make sure to follow us on all our social media outlets to stay as up to date as possible with everything!

Youtube
Patreon
Github
Facebook
Twitter
Instagram


Salesforce Development Books I Recommend

Advanced Apex Programming
Salesforce Lightning Platform Enterprise Architecture
Mastering Salesforce DevOps

Good Non-SF Specific Development Books:

Clean Code
Clean Architecture