# Fixing inefficient Apex: loops, collections and caching

*Platform Developer II Study Guide · Performance · Last updated October 16, 2026 · https://www.certifyforce.com/study-guide/platform-developer-ii/performance/fixing-inefficient-apex-loops-collections-and-caching*

Read an Apex snippet, find the limit it will hit and apply the matching fix: sets and maps instead of queries in loops, map lookups instead of nested loops, values computed once outside the loop, COUNT() instead of loaded rows, static and Platform Cache caching, before-trigger field updates, changed-field guards and one bulk sendEmail call.

- Certification: Salesforce Platform Developer II
- Exam section: Performance
- Study guide: https://www.certifyforce.com/study-guide/platform-developer-ii

**Official exam objective:**

> Given sample code, identify inefficiencies and demonstrate how to resolve them.

## What the exam expects

- Spot SOQL, DML, describe calls or `sendEmail` inside a loop, even hidden in a helper, and rewrite with sets, maps or one bulk call.
- Replace nested loops that exhaust CPU time with map lookups.
- Remove work that returns the same value for every record: move it before the loop or use a no-query API such as `UserInfo`.
- Choose `COUNT()`, a static variable or Platform Cache to avoid loading or re-querying data, and use the cache API efficiently.
- Cut unnecessary saves with before-trigger field updates and changed-field guards.

Key terms: **bulkification** (code that handles a whole collection in one pass with constant queries and DML), **loop-invariant work** (a computation whose result doesn't depend on the loop's record), **static variable** (a class variable shared for one transaction), **Platform Cache** (a cache that survives across transactions), **changed-field guard** (comparing `Trigger.new` to `Trigger.oldMap`).

## Key facts

| Fact | Detail | Source |
|---|---|---|
| SOQL queries per transaction | 100 synchronous, 200 asynchronous | [Execution Governors and Limits](https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_gov_limits.htm) |
| Rows retrieved by SOQL | 50,000 per transaction | [Execution Governors and Limits](https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_gov_limits.htm) |
| CPU time | 10,000 ms synchronous, 60,000 ms asynchronous | [Execution Governors and Limits](https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_gov_limits.htm) |
| `sendEmail` calls | 10 per transaction | [Outbound Email](https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_forcecom_email_outbound.htm) |
| `COUNT()` row cost | One row without `GROUP BY`; one per group with it | [Working with SOQL Aggregate Functions](https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/langCon_apex_SOQL_agg_fns.htm) |
| Context user's role | `UserInfo.getUserRoleId()`, no query | [UserInfo Class](https://developer.salesforce.com/docs/atlas.en-us.apexref.meta/apexref/apex_methods_system_userinfo.htm) |
| Static variable lifetime | One transaction; reset at the boundary | [Static Variables](https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_classes_static.htm) |
| Platform Cache lookup | `get(key)` and test for null, not `contains` then `get` | [Platform Cache Best Practices](https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_platform_cache_best_practices.htm) |
| Full partition | Least recently used keys evicted | [Platform Cache Best Practices](https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_platform_cache_best_practices.htm) |
| `Trigger.new` changes | Saved without DML only in before triggers | [Context Variable Considerations](https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_triggers_context_variables_considerations.htm) |

## Check yourself

- Why does caching each warehouse in a static map inside a single-ID helper still fail for 150 distinct warehouses?
- When does a lazy-loaded static variable re-run its query?
- Why is a before trigger faster than an after trigger for setting a field on the saved records?
- How many query rows does `SELECT COUNT() FROM Case WHERE ...` consume?
- Why should Platform Cache code call `get(key)` and test for null instead of `contains(key)` first?

## Sources

- [Trigger and Bulk Request Best Practices](https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_triggers_bestpract.htm)
- [Common Bulk Trigger Idioms](https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_triggers_bulk_idioms.htm)
- [Execution Governors and Limits](https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_gov_limits.htm)
- [UserInfo Class](https://developer.salesforce.com/docs/atlas.en-us.apexref.meta/apexref/apex_methods_system_userinfo.htm)
- [Understanding Apex Describe Information](https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_dynamic_describe_objects_understanding.htm)
- [Accessing All sObjects](https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_dynamic_global_describe.htm)
- [Working with SOQL Aggregate Functions](https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/langCon_apex_SOQL_agg_fns.htm)
- [Static and Instance Methods, Variables, and Initialization Code](https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_classes_static.htm)
- [Platform Cache Best Practices](https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_platform_cache_best_practices.htm)
- [Triggers](https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_triggers.htm)
- [Context Variable Considerations](https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_triggers_context_variables_considerations.htm)
- [Outbound Email](https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_forcecom_email_outbound.htm)

## About the full unit

The "Core concepts", "Exam traps" and "Worked scenario" sections of this unit, its 11 linked practice questions, and unlimited timed mock exams are included with the Platform Developer II pack (USD $19): https://www.certifyforce.com/practice-exams
