Salesforce · · 21 min read

Conditional Statements and Collection Iteration in Apex

Master control flow in Apex — if/else statements, switch statements, for loops, enhanced for loops, while and do-while loops, and best practices for iterating over collections efficiently.

Part 38: Conditional Statements and Collection Iteration in Apex

Welcome back to the Salesforce series. In the previous installment we covered the foundational building blocks of Apex — data types, variables, operators, and the structure of an Apex class. Now it is time to put those pieces in motion. Every meaningful program needs to make decisions and repeat work across collections of data. In Salesforce, where triggers fire on batches of up to 200 records at a time, knowing how to write efficient conditionals and loops is not optional — it is survival.

This is Part 38 of the series, and it is dedicated entirely to control flow. We will start with conditional statements, move through every loop construct Apex offers, and finish with the best practices that separate production-ready code from code that crashes at scale. There is a hands-on project at the end, so stick around.


Conditional Statements (If/Else)

Conditional statements let your code take different paths depending on whether a condition evaluates to true or false. The if/else construct is the most fundamental decision-making tool in Apex.

Basic Syntax

if (condition) {
    // executes when condition is true
} else if (anotherCondition) {
    // executes when anotherCondition is true
} else {
    // executes when none of the above are true
}

The condition inside the parentheses must evaluate to a Boolean. Unlike JavaScript, Apex does not allow truthy or falsy values — you cannot write if (someString). It must be an explicit Boolean expression.

A Simple Example

Integer score = 85;

if (score >= 90) {
    System.debug('Grade: A');
} else if (score >= 80) {
    System.debug('Grade: B');
} else if (score >= 70) {
    System.debug('Grade: C');
} else {
    System.debug('Grade: F');
}
// Output: Grade: B

Null Checks

Null reference exceptions are one of the most common runtime errors in Apex. Always check for null before accessing properties or methods on an object.

Account acc = [SELECT Name, Industry FROM Account WHERE Id = :someId LIMIT 1];

// Dangerous — if acc is null, this throws a NullPointerException
// String industry = acc.Industry;

// Safe approach
if (acc != null && acc.Industry != null) {
    System.debug('Industry: ' + acc.Industry);
} else {
    System.debug('No industry set.');
}

Apex evaluates Boolean expressions using short-circuit evaluation. In the example above, if acc is null, the second condition (acc.Industry != null) is never evaluated, which prevents the null pointer exception.

Nested Conditionals

You can nest if statements inside each other, but keep the nesting shallow. Deeply nested conditionals are hard to read and maintain.

if (acc != null) {
    if (acc.Industry == 'Technology') {
        if (acc.AnnualRevenue > 1000000) {
            acc.Rating = 'Hot';
        } else {
            acc.Rating = 'Warm';
        }
    }
}

A cleaner approach is to combine conditions or use early returns:

if (acc == null || acc.Industry != 'Technology') {
    return;
}

acc.Rating = (acc.AnnualRevenue > 1000000) ? 'Hot' : 'Warm';

Common Patterns with sObjects

When working with Salesforce records in triggers and classes, certain conditional patterns come up repeatedly.

Checking Record Type:

if (opp.RecordTypeId == Schema.SObjectType.Opportunity.getRecordTypeInfosByDeveloperName()
        .get('Enterprise').getRecordTypeId()) {
    // logic for Enterprise opportunities
}

Checking Field Changes in a Trigger:

for (Account newAcc : Trigger.new) {
    Account oldAcc = Trigger.oldMap.get(newAcc.Id);

    if (newAcc.OwnerId != oldAcc.OwnerId) {
        // Owner changed — do something
    }

    if (newAcc.Industry != oldAcc.Industry) {
        // Industry changed — do something
    }
}

The Ternary Operator:

For simple two-way decisions, the ternary operator keeps code compact:

String priority = (caseRecord.Priority == 'High') ? 'Urgent' : 'Normal';

Switch Statements

Apex supports switch statements, which provide a cleaner alternative to long chains of if/else if blocks when you are comparing a single value against multiple possibilities.

Basic Syntax

switch on expression {
    when value1 {
        // code for value1
    }
    when value2, value3 {
        // code for value2 or value3
    }
    when else {
        // code when no other value matches
    }
}

Notice that Apex uses switch on (not just switch), and there is no break statement needed. Each when block is self-contained — there is no fall-through behavior like in Java or C.

Switch on String

String region = account.BillingCountry;

switch on region {
    when 'United States' {
        taxRate = 0.08;
    }
    when 'Canada' {
        taxRate = 0.13;
    }
    when 'United Kingdom' {
        taxRate = 0.20;
    }
    when else {
        taxRate = 0.15;
    }
}

Switch on Integer

Integer quarter = getQuarterFromDate(Date.today());

switch on quarter {
    when 1 {
        System.debug('Q1 — Focus on pipeline generation.');
    }
    when 2 {
        System.debug('Q2 — Mid-year review.');
    }
    when 3 {
        System.debug('Q3 — Prepare forecasts.');
    }
    when 4 {
        System.debug('Q4 — Close deals.');
    }
    when else {
        System.debug('Invalid quarter.');
    }
}

Switch on Enum

Enums and switch statements are a natural fit. Define an Enum for a fixed set of values, then branch on it cleanly.

public enum Season { SPRING, SUMMER, FALL, WINTER }

Season current = Season.SUMMER;

switch on current {
    when SPRING {
        System.debug('Launch spring campaigns.');
    }
    when SUMMER {
        System.debug('Summer promotions active.');
    }
    when FALL {
        System.debug('Prepare holiday inventory.');
    }
    when WINTER {
        System.debug('Year-end close activities.');
    }
}

Note that Enum values in when clauses do not use the Enum class prefix — you write when SPRING, not when Season.SPRING.

Switch on sObject Type

One of the most powerful features of Apex switch statements is the ability to branch based on the runtime type of an sObject. This is invaluable when writing utility methods that accept a generic SObject parameter.

public static void logRecordInfo(SObject record) {
    switch on record {
        when Account acc {
            System.debug('Account: ' + acc.Name);
        }
        when Contact con {
            System.debug('Contact: ' + con.FirstName + ' ' + con.LastName);
        }
        when Opportunity opp {
            System.debug('Opportunity: ' + opp.Name + ' — Stage: ' + opp.StageName);
        }
        when else {
            System.debug('Unknown sObject type.');
        }
    }
}

In the when Account acc clause, Apex both checks the type and casts the variable for you. Inside that block you have access to all Account fields through the acc variable without any manual casting.

The when else Clause

The when else clause is optional but recommended. It acts as a safety net for unexpected values. Without it, if no when block matches, the switch statement simply does nothing — no error is thrown, which can make bugs silent and hard to track.


When to Use Switch vs. Conditionals

Both constructs achieve the same goal, but each has strengths.

FactorIf/ElseSwitch
ReadabilityBest for 1-3 conditions or complex Boolean logicBest for 4+ discrete values against one variable
Complex conditionsSupports compound expressions (&&, `
Type-based branchingRequires manual instanceof checks and castingBuilt-in sObject type matching with auto-cast
Null handlingFlexible — you write the null check yourselfA null expression matches when else
Range checksSupported directly (score >= 80 && score < 90)Not supported — use if/else for ranges
Enum valuesWorks but verboseClean and concise
PerformanceEvaluates conditions sequentiallyCompiler can optimize lookup for large value sets

Use if/else when your conditions involve ranges, compound Boolean expressions, or comparisons across multiple variables.

Use switch when you are comparing a single variable against a known set of discrete values, especially Strings, Integers, Enums, or sObject types.


What Is a For Loop

A for loop repeats a block of code a specific number of times. It is the workhorse of iteration in every programming language, and Apex is no different.

Every for loop has three components:

  1. Initialization — sets the starting value of the loop variable.
  2. Condition — checked before each iteration. The loop continues as long as this is true.
  3. Increment — updates the loop variable after each iteration.
for (initialization; condition; increment) {
    // code to repeat
}

The execution order is: initialize once, check condition, execute body, increment, check condition, execute body, increment, and so on until the condition is false.


Creating a Basic For Loop

The traditional for loop uses an Integer counter to control iteration.

Counting from 0 to 9

for (Integer i = 0; i < 10; i++) {
    System.debug('Iteration: ' + i);
}

Iterating Over a List by Index

List<String> cities = new List<String>{'New York', 'San Francisco', 'Chicago', 'Austin'};

for (Integer i = 0; i < cities.size(); i++) {
    System.debug('City ' + (i + 1) + ': ' + cities[i]);
}

Building a List Programmatically

List<Account> accountsToInsert = new List<Account>();

for (Integer i = 1; i <= 50; i++) {
    accountsToInsert.add(new Account(
        Name = 'Test Account ' + i,
        Industry = 'Technology'
    ));
}

insert accountsToInsert;

Counting Backwards

for (Integer i = 10; i > 0; i--) {
    System.debug('Countdown: ' + i);
}
System.debug('Liftoff!');

Stepping by More Than One

for (Integer i = 0; i <= 100; i += 10) {
    System.debug('Value: ' + i);
}
// Output: 0, 10, 20, 30, ... 100

Creating an Enhanced For Loop

The enhanced for loop (also called a for-each loop) is the most commonly used loop in Apex. It iterates directly over the elements of a collection without needing an index variable.

Syntax

for (DataType element : collection) {
    // work with element
}

Iterating Over a List

List<Contact> contacts = [SELECT FirstName, LastName, Email FROM Contact LIMIT 10];

for (Contact con : contacts) {
    System.debug(con.FirstName + ' ' + con.LastName + ' — ' + con.Email);
}

Iterating Over a Set

Set<String> industries = new Set<String>{'Technology', 'Finance', 'Healthcare'};

for (String industry : industries) {
    System.debug('Industry: ' + industry);
}

Note that Sets are unordered, so the iteration order is not guaranteed.

Iterating Over Map Keys

Map<Id, Account> accountMap = new Map<Id, Account>(
    [SELECT Id, Name, Industry FROM Account LIMIT 10]
);

for (Id accountId : accountMap.keySet()) {
    Account acc = accountMap.get(accountId);
    System.debug(acc.Name + ' (' + acc.Industry + ')');
}

Iterating Over Map Values

for (Account acc : accountMap.values()) {
    System.debug(acc.Name);
}

SOQL For Loop

Apex offers a special variant of the enhanced for loop designed specifically for SOQL queries. Instead of loading all results into memory at once, it retrieves records in batches of 200 automatically. This is critical for working with large data volumes.

for (Account acc : [SELECT Id, Name, Industry FROM Account WHERE Industry = 'Technology']) {
    System.debug(acc.Name);
}

For even more control, you can iterate in batches of 200 using a List type:

for (List<Account> accountBatch : [SELECT Id, Name FROM Account]) {
    System.debug('Processing batch of ' + accountBatch.size() + ' accounts.');
    // process each batch
    for (Account acc : accountBatch) {
        acc.Description = 'Processed on ' + Date.today();
    }
    update accountBatch;
}

This batched SOQL for loop is the preferred pattern when updating large numbers of records because it keeps heap size under control.


While and Do-While Loops

While Loop

A while loop repeats as long as its condition is true. The condition is checked before each iteration, which means the body may never execute.

Integer count = 0;

while (count < 5) {
    System.debug('Count: ' + count);
    count++;
}

Do-While Loop

A do-while loop is similar, but the condition is checked after each iteration. This guarantees the body executes at least once.

Integer count = 0;

do {
    System.debug('Count: ' + count);
    count++;
} while (count < 5);

When to Use While vs. For

ScenarioBest Loop
Known number of iterationsTraditional for loop
Iterating over a collectionEnhanced for loop
Unknown iterations, condition checked firstWhile loop
Unknown iterations, must run at least onceDo-while loop

While loops are useful when the number of iterations depends on a dynamic condition — for example, polling a status until it changes, or processing a queue until it is empty.

List<String> queue = new List<String>{'Task A', 'Task B', 'Task C'};

while (!queue.isEmpty()) {
    String current = queue.remove(0);
    System.debug('Processing: ' + current);
}

Avoiding Infinite Loops

A while loop that never has its condition become false will run forever and eventually hit the Apex CPU time limit (10,000 ms synchronous, 60,000 ms asynchronous). Always ensure your loop has a clear exit condition.

// DANGEROUS — if someCondition never becomes false, this runs until CPU limit
while (someCondition) {
    // missing logic to change someCondition
}

// SAFE — add a guard counter
Integer maxIterations = 1000;
Integer iterations = 0;

while (someCondition && iterations < maxIterations) {
    // process
    iterations++;
}

Important Best Practices for Collection Iteration

This section is arguably the most important part of this post. Salesforce enforces strict governor limits on every transaction. If your loops violate these limits, your code will fail in production — sometimes silently, sometimes catastrophically. The best practices below are non-negotiable for any Apex developer.

1. Never Put SOQL Queries Inside Loops

This is the number one rule of Apex development. The governor limit allows 100 SOQL queries per synchronous transaction. If your trigger fires on 200 records and each iteration runs a query, you will hit 200 queries and the transaction will fail.

Bad — SOQL inside a loop:

// DO NOT DO THIS
for (Opportunity opp : Trigger.new) {
    Account acc = [SELECT Name, Industry FROM Account WHERE Id = :opp.AccountId];
    opp.Description = 'Account: ' + acc.Name;
}

This code runs one query per Opportunity. With 200 Opportunities in a batch, that is 200 queries — twice the limit.

Good — query once, use a Map:

// Collect all Account IDs first
Set<Id> accountIds = new Set<Id>();
for (Opportunity opp : Trigger.new) {
    accountIds.add(opp.AccountId);
}

// Single query outside the loop
Map<Id, Account> accountMap = new Map<Id, Account>(
    [SELECT Id, Name, Industry FROM Account WHERE Id IN :accountIds]
);

// Process using the Map
for (Opportunity opp : Trigger.new) {
    Account acc = accountMap.get(opp.AccountId);
    if (acc != null) {
        opp.Description = 'Account: ' + acc.Name;
    }
}

This pattern uses exactly one SOQL query regardless of how many records are in the trigger.

2. Never Put DML Statements Inside Loops

The governor limit allows 150 DML statements per transaction. Inserting, updating, or deleting one record at a time inside a loop burns through this limit quickly.

Bad — DML inside a loop:

// DO NOT DO THIS
for (Contact con : contactsToUpdate) {
    con.MailingCity = 'San Francisco';
    update con;
}

Good — collect, then perform DML once:

for (Contact con : contactsToUpdate) {
    con.MailingCity = 'San Francisco';
}
update contactsToUpdate;

3. Use the Collect, Query, Process Pattern

This is the standard pattern for writing bulkified Apex, especially in triggers. It has three phases:

  1. Collect — gather the IDs or values you need from the incoming records.
  2. Query — run a single SOQL query (or a small number of queries) to fetch related data.
  3. Process — iterate over the records and apply logic using the queried data.
// PHASE 1: Collect
Set<Id> ownerIds = new Set<Id>();
for (Case c : Trigger.new) {
    ownerIds.add(c.OwnerId);
}

// PHASE 2: Query
Map<Id, User> ownerMap = new Map<Id, User>(
    [SELECT Id, Name, Email, ManagerId FROM User WHERE Id IN :ownerIds]
);

// PHASE 3: Process
List<Task> tasksToCreate = new List<Task>();
for (Case c : Trigger.new) {
    User owner = ownerMap.get(c.OwnerId);
    if (owner != null && owner.ManagerId != null) {
        tasksToCreate.add(new Task(
            Subject = 'Review new case: ' + c.Subject,
            WhoId = c.ContactId,
            OwnerId = owner.ManagerId,
            WhatId = c.Id,
            ActivityDate = Date.today().addDays(3)
        ));
    }
}

// Single DML outside all loops
if (!tasksToCreate.isEmpty()) {
    insert tasksToCreate;
}

4. Use Maps for O(1) Lookups Instead of Nested Loops

Nested loops that search for matching records have O(n*m) complexity. With large data sets, this leads to CPU timeout failures. Maps reduce the lookup to O(1).

Bad — nested loop for matching:

// DO NOT DO THIS — O(n*m) complexity
List<Account> accounts = [SELECT Id, Name FROM Account];
List<Opportunity> opps = [SELECT Id, Name, AccountId FROM Opportunity];

for (Opportunity opp : opps) {
    for (Account acc : accounts) {
        if (opp.AccountId == acc.Id) {
            System.debug(opp.Name + ' belongs to ' + acc.Name);
            break;
        }
    }
}

Good — Map lookup:

Map<Id, Account> accountMap = new Map<Id, Account>(
    [SELECT Id, Name FROM Account]
);

for (Opportunity opp : [SELECT Id, Name, AccountId FROM Opportunity]) {
    Account acc = accountMap.get(opp.AccountId);
    if (acc != null) {
        System.debug(opp.Name + ' belongs to ' + acc.Name);
    }
}

5. Be Aware of Governor Limits in Every Loop

Here is a quick reference of the limits most relevant to loops:

LimitSynchronousAsynchronous
SOQL queries100200
DML statements150150
Records retrieved by SOQL50,00050,000
Records processed by DML10,00010,000
CPU time10,000 ms60,000 ms
Heap size6 MB12 MB

Every query, DML statement, and CPU cycle consumed inside a loop multiplies by the number of iterations. Always assume your trigger will receive the maximum batch size of 200 records and design accordingly.

6. Avoid Hardcoding IDs

Never hardcode Salesforce record IDs in your code. IDs differ between sandbox and production environments.

Bad:

if (acc.RecordTypeId == '012000000000ABC') {
    // fragile — this ID will break in another org
}

Good:

Id targetRecordTypeId = Schema.SObjectType.Account
    .getRecordTypeInfosByDeveloperName()
    .get('Enterprise')
    .getRecordTypeId();

if (acc.RecordTypeId == targetRecordTypeId) {
    // safe — works in any org
}

7. Check Collection Size Before DML

Performing DML on an empty list does not throw an error, but it does count as a DML statement against your governor limits. Check the size first.

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

// ... loop that conditionally adds contacts ...

if (!contactsToUpdate.isEmpty()) {
    update contactsToUpdate;
}

8. Use break and continue Wisely

  • break exits the loop entirely.
  • continue skips the rest of the current iteration and moves to the next one.
for (Lead lead : leads) {
    if (lead.Email == null) {
        continue; // skip leads without email
    }

    if (lead.Status == 'Converted') {
        break; // stop processing once we hit a converted lead
    }

    // process the lead
}

PROJECT: The Automatic Account Creator

Let us put everything together. In this project you will write an Apex class that creates a batch of Account records with different properties based on conditions. This exercise combines conditionals, loops, switch statements, Maps, and best practices.

The Requirements

Write a class called AutomaticAccountCreator with a method createAccounts that:

  1. Accepts an Integer numberOfAccounts (how many to create) and a String region.
  2. Creates Account records with names like “Auto Account 1”, “Auto Account 2”, etc.
  3. Assigns the Industry based on the account number: accounts 1-10 get “Technology”, 11-20 get “Finance”, 21-30 get “Healthcare”, and everything above 30 gets “Other”.
  4. Assigns the BillingCountry based on the region parameter using a switch statement.
  5. Assigns the Rating based on the account number: multiples of 5 are “Hot”, even numbers are “Warm”, and odd numbers are “Cold”.
  6. After inserting the accounts, queries them back and creates a Map of Account Name to Account, then logs a summary.

The Solution

public class AutomaticAccountCreator {

    public static List<Account> createAccounts(Integer numberOfAccounts, String region) {
        // Validate input
        if (numberOfAccounts == null || numberOfAccounts <= 0) {
            System.debug('Invalid number of accounts. Must be a positive integer.');
            return new List<Account>();
        }

        // Determine billing country using a switch statement
        String billingCountry;
        switch on region {
            when 'US' {
                billingCountry = 'United States';
            }
            when 'UK' {
                billingCountry = 'United Kingdom';
            }
            when 'CA' {
                billingCountry = 'Canada';
            }
            when 'AU' {
                billingCountry = 'Australia';
            }
            when else {
                billingCountry = 'Unknown';
            }
        }

        // Build the list of accounts using a traditional for loop
        List<Account> accountsToInsert = new List<Account>();

        for (Integer i = 1; i <= numberOfAccounts; i++) {
            Account acc = new Account();
            acc.Name = 'Auto Account ' + i;
            acc.BillingCountry = billingCountry;

            // Assign industry using if/else conditionals
            if (i <= 10) {
                acc.Industry = 'Technology';
            } else if (i <= 20) {
                acc.Industry = 'Finance';
            } else if (i <= 30) {
                acc.Industry = 'Healthcare';
            } else {
                acc.Industry = 'Other';
            }

            // Assign rating using conditionals
            // Check multiples of 5 first (more specific), then even/odd
            if (Math.mod(i, 5) == 0) {
                acc.Rating = 'Hot';
            } else if (Math.mod(i, 2) == 0) {
                acc.Rating = 'Warm';
            } else {
                acc.Rating = 'Cold';
            }

            accountsToInsert.add(acc);
        }

        // Single DML operation outside the loop
        if (!accountsToInsert.isEmpty()) {
            insert accountsToInsert;
            System.debug('Inserted ' + accountsToInsert.size() + ' accounts.');
        }

        // Query the inserted accounts back and build a Map
        Map<String, Account> accountsByName = new Map<String, Account>();
        for (Account acc : [SELECT Id, Name, Industry, Rating, BillingCountry
                            FROM Account
                            WHERE Id IN :accountsToInsert]) {
            accountsByName.put(acc.Name, acc);
        }

        // Log a summary using an enhanced for loop over the Map
        Integer hotCount = 0;
        Integer warmCount = 0;
        Integer coldCount = 0;

        for (String accName : accountsByName.keySet()) {
            Account acc = accountsByName.get(accName);

            switch on acc.Rating {
                when 'Hot' {
                    hotCount++;
                }
                when 'Warm' {
                    warmCount++;
                }
                when 'Cold' {
                    coldCount++;
                }
            }
        }

        System.debug('--- Account Creation Summary ---');
        System.debug('Total created: ' + accountsByName.size());
        System.debug('Hot: ' + hotCount + ' | Warm: ' + warmCount + ' | Cold: ' + coldCount);
        System.debug('Region: ' + region + ' (' + billingCountry + ')');
        System.debug('--------------------------------');

        return accountsToInsert;
    }
}

Running the Project

Open the Developer Console, click Debug > Open Execute Anonymous Window, and run:

AutomaticAccountCreator.createAccounts(35, 'US');

Check the debug log. You should see 35 accounts created with the correct Industry, Rating, and BillingCountry values. Open the Accounts tab and verify the records are there.

What This Project Demonstrates

  • Traditional for loop for creating a known number of records.
  • If/else conditionals for assigning Industry based on ranges.
  • Switch statement for mapping a region code to a country name.
  • Math.mod for checking divisibility (multiples and even/odd).
  • Single DML outside the loop — no matter how many accounts we create, we only call insert once.
  • Enhanced for loop over a Map’s keySet to iterate over queried results.
  • Switch on String for counting ratings in the summary.
  • Collect, query, process pattern — we build the list, insert it, then query back and process the results.

Extending the Project

Try these challenges on your own:

  1. Add a Date field (SLAExpirationDate__c) that is set to 30 days from today for Hot accounts, 60 days for Warm, and 90 days for Cold.
  2. Add error handling — wrap the DML in a try-catch block and handle DmlException.
  3. Create a companion method deleteAutoAccounts() that queries all accounts where the name starts with “Auto Account” and deletes them in a single DML operation.
  4. Modify the method to accept a List<String> of regions and create accounts for each region, using a nested loop — but still performing only one insert at the end.

Summary

This post covered the full spectrum of control flow in Apex:

  • If/else statements handle Boolean-based decisions, null checks, and complex conditional logic.
  • Switch statements provide clean branching on discrete values — Strings, Integers, Enums, and even sObject types.
  • Traditional for loops give you precise control when you need a counter or index.
  • Enhanced for loops are the go-to for iterating over Lists, Sets, Maps, and SOQL results.
  • SOQL for loops handle large query results in memory-efficient batches of 200.
  • While and do-while loops are for situations where the number of iterations is not known in advance.
  • Best practices — no SOQL in loops, no DML in loops, use Maps for lookups, follow the collect-query-process pattern, and always design for bulk.

These patterns are not just coding conventions. They are requirements imposed by the Salesforce governor limits. Internalizing them now will save you from painful debugging later.


In the next post, Part 39, we will dive into SOQL and SOSL in Salesforce — how to query data from the database using Salesforce Object Query Language and Salesforce Object Search Language. We will cover basic queries, filters, relationships, aggregate functions, dynamic SOQL, and the differences between SOQL and SOSL. See you there.